Update files
This commit is contained in:
+6
-4
@@ -23,12 +23,14 @@
|
||||
"format:check": "prettier --check .",
|
||||
"start": "npm run start --workspace=@getpaseo/server",
|
||||
"android": "npm run android --workspace=@getpaseo/app",
|
||||
"android:release": "ANDROID_VARIANT=productionRelease npm run android --workspace=@getpaseo/app",
|
||||
"android:dev-release": "ANDROID_VARIANT=devRelease npm run android --workspace=@getpaseo/app",
|
||||
"android:fast": "npm run android:fast --workspace=@getpaseo/app",
|
||||
"android:debug": "npm run android:debug --workspace=@getpaseo/app",
|
||||
"android:prod": "npm run android:prod --workspace=@getpaseo/app",
|
||||
"android:release": "npm run android:prod --workspace=@getpaseo/app",
|
||||
"ios": "npm run ios --workspace=@getpaseo/app",
|
||||
"web": "npm run web --workspace=@getpaseo/app",
|
||||
"desktop": "npm run dev --workspace=@getpaseo/desktop",
|
||||
"desktop:build": "npm run build --workspace=@getpaseo/desktop",
|
||||
"dev:desktop": "npm run dev --workspace=@getpaseo/desktop",
|
||||
"build:desktop": "npm run build --workspace=@getpaseo/desktop",
|
||||
"cli": "npx tsx packages/cli/src/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
// App name and package ID are controlled by Gradle product flavors (dev/production)
|
||||
// See plugins/with-android-product-flavors.js for flavor configuration
|
||||
|
||||
const pkg = require("./package.json");
|
||||
const appVariant = process.env.APP_VARIANT ?? "prod";
|
||||
|
||||
const variants = {
|
||||
prod: {
|
||||
name: "Paseo",
|
||||
packageId: "sh.paseo",
|
||||
},
|
||||
debug: {
|
||||
name: "Paseo Debug",
|
||||
packageId: "sh.paseo.debug",
|
||||
},
|
||||
fast: {
|
||||
name: "Paseo Dev",
|
||||
packageId: "sh.paseo.dev",
|
||||
},
|
||||
};
|
||||
|
||||
const variant = variants[appVariant] ?? variants.prod;
|
||||
|
||||
export default {
|
||||
expo: {
|
||||
name: "Paseo",
|
||||
name: variant.name,
|
||||
slug: "voice-mobile",
|
||||
version: pkg.version,
|
||||
orientation: "portrait",
|
||||
@@ -26,7 +41,7 @@ export default {
|
||||
"This app needs access to the microphone for voice commands.",
|
||||
ITSAppUsesNonExemptEncryption: false,
|
||||
},
|
||||
bundleIdentifier: "sh.paseo",
|
||||
bundleIdentifier: variant.packageId,
|
||||
},
|
||||
android: {
|
||||
adaptiveIcon: {
|
||||
@@ -45,8 +60,7 @@ export default {
|
||||
"CAMERA",
|
||||
"android.permission.CAMERA",
|
||||
],
|
||||
// Base package - Gradle product flavors override this per variant
|
||||
package: "sh.paseo",
|
||||
package: variant.packageId,
|
||||
},
|
||||
web: {
|
||||
output: "single",
|
||||
@@ -91,7 +105,6 @@ export default {
|
||||
},
|
||||
},
|
||||
],
|
||||
"./plugins/with-android-product-flavors",
|
||||
],
|
||||
experiments: {
|
||||
typedRoutes: true,
|
||||
|
||||
+23
-7
@@ -3,20 +3,36 @@
|
||||
"version": ">= 14.0.0"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"debug": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal",
|
||||
"channel": "development"
|
||||
"channel": "debug",
|
||||
"env": {
|
||||
"APP_VARIANT": "debug"
|
||||
},
|
||||
"android": {
|
||||
"gradleCommand": ":app:assembleDebug"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"fast": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal",
|
||||
"channel": "preview"
|
||||
"channel": "fast",
|
||||
"env": {
|
||||
"APP_VARIANT": "fast"
|
||||
},
|
||||
"android": {
|
||||
"gradleCommand": ":app:assembleDebugOptimized"
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"channel": "production"
|
||||
"prod": {
|
||||
"channel": "production",
|
||||
"env": {
|
||||
"APP_VARIANT": "prod"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
"prod": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "ANDROID_VARIANT=${ANDROID_VARIANT:-devDebug} expo run:android --variant=${ANDROID_VARIANT:-devDebug}",
|
||||
"android:release": "ANDROID_VARIANT=${ANDROID_VARIANT:-productionRelease} expo run:android --variant=${ANDROID_VARIANT:-productionRelease}",
|
||||
"android": "npm run android:fast",
|
||||
"android:fast": "APP_VARIANT=fast expo run:android --variant=debugOptimized",
|
||||
"android:debug": "APP_VARIANT=debug expo run:android --variant=debug",
|
||||
"android:prod": "APP_VARIANT=prod expo run:android --variant=release",
|
||||
"android:release": "npm run android:prod",
|
||||
"ios": "expo run:ios",
|
||||
"ios:release": "expo run:ios --configuration Release",
|
||||
"web": "expo start --web",
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
const { withAppBuildGradle } = require("expo/config-plugins");
|
||||
|
||||
/**
|
||||
* Expo config plugin to add Android product flavors (dev/production)
|
||||
* This allows building both dev and production variants without re-running prebuild
|
||||
*
|
||||
* Variants created:
|
||||
* - devDebug: Development build with dev tools
|
||||
* - devRelease: Development build optimized
|
||||
* - productionDebug: Production build with dev tools
|
||||
* - productionRelease: Production build optimized (for app store)
|
||||
*/
|
||||
function withAndroidProductFlavors(config) {
|
||||
return withAppBuildGradle(config, (config) => {
|
||||
let buildGradle = config.modResults.contents;
|
||||
|
||||
// Add debuggableVariants to the react block
|
||||
buildGradle = buildGradle.replace(
|
||||
'// debuggableVariants = ["liteDebug", "prodDebug"]',
|
||||
'debuggableVariants = ["devDebug", "productionDebug"]'
|
||||
);
|
||||
|
||||
// Add product flavors after signingConfigs block
|
||||
const signingConfigsEnd = buildGradle.indexOf(
|
||||
" }\n buildTypes {",
|
||||
buildGradle.indexOf("signingConfigs {")
|
||||
);
|
||||
|
||||
if (signingConfigsEnd === -1) {
|
||||
console.warn(
|
||||
"Could not find signingConfigs end in build.gradle, skipping product flavors"
|
||||
);
|
||||
return config;
|
||||
}
|
||||
|
||||
const productFlavorsBlock = ` }
|
||||
flavorDimensions "environment"
|
||||
productFlavors {
|
||||
dev {
|
||||
dimension "environment"
|
||||
applicationId "sh.paseo.dev"
|
||||
resValue "string", "app_name", "Paseo (Dev)"
|
||||
}
|
||||
production {
|
||||
dimension "environment"
|
||||
applicationId "sh.paseo"
|
||||
resValue "string", "app_name", "Paseo"
|
||||
}
|
||||
}
|
||||
buildTypes {`;
|
||||
|
||||
buildGradle = buildGradle.replace(
|
||||
" }\n buildTypes {",
|
||||
productFlavorsBlock
|
||||
);
|
||||
|
||||
// Remove applicationId from defaultConfig since flavors handle it
|
||||
buildGradle = buildGradle.replace(
|
||||
/applicationId ['"]com\.moboudra\.paseo['"]\n\s*/,
|
||||
""
|
||||
);
|
||||
|
||||
config.modResults.contents = buildGradle;
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = withAndroidProductFlavors;
|
||||
@@ -357,7 +357,7 @@ describe("curateAgentActivity", () => {
|
||||
];
|
||||
|
||||
const result = curateAgentActivity(timeline);
|
||||
expect(result).toBe('[speak] {"text":"hello from voice"}');
|
||||
expect(result).toBe('[Speak] {"text":"hello from voice"}');
|
||||
});
|
||||
|
||||
test("shows MCP tool input JSON", () => {
|
||||
@@ -389,7 +389,22 @@ describe("curateAgentActivity", () => {
|
||||
];
|
||||
|
||||
const result = curateAgentActivity(timeline);
|
||||
expect(result).toBe('[paseo_voice.speak] {"text":"hello from namespaced tool"}');
|
||||
expect(result).toBe('[Speak] {"text":"hello from namespaced tool"}');
|
||||
});
|
||||
|
||||
test("shows claude mcp speak tool input as Speak", () => {
|
||||
const timeline: AgentTimelineItem[] = [
|
||||
{
|
||||
type: "tool_call",
|
||||
callId: "m3",
|
||||
name: "mcp__paseo_voice__speak",
|
||||
input: { text: "hello from claude mcp" },
|
||||
status: "completed",
|
||||
},
|
||||
];
|
||||
|
||||
const result = curateAgentActivity(timeline);
|
||||
expect(result).toBe('[Speak] {"text":"hello from claude mcp"}');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AgentTimelineItem } from "./agent-sdk-types.js";
|
||||
import { extractPrincipalParam } from "../../utils/tool-call-parsers.js";
|
||||
import { extractPrincipalParam, normalizeToolDisplayName } from "../../utils/tool-call-parsers.js";
|
||||
import { isLikelyExternalToolName } from "./tool-name-normalization.js";
|
||||
|
||||
const DEFAULT_MAX_ITEMS = 40;
|
||||
@@ -147,16 +147,17 @@ export function curateAgentActivity(
|
||||
break;
|
||||
case "tool_call": {
|
||||
flushBuffers(lines, buffers);
|
||||
const displayName = normalizeToolDisplayName(item.name);
|
||||
const inputJson = formatToolInputJson(item.input);
|
||||
if (isLikelyExternalToolName(item.name) && inputJson) {
|
||||
lines.push(`[${item.name}] ${inputJson}`);
|
||||
lines.push(`[${displayName}] ${inputJson}`);
|
||||
break;
|
||||
}
|
||||
const principal = extractPrincipalParam(item.input);
|
||||
if (principal) {
|
||||
lines.push(`[${item.name}] ${principal}`);
|
||||
lines.push(`[${displayName}] ${principal}`);
|
||||
} else {
|
||||
lines.push(`[${item.name}]`);
|
||||
lines.push(`[${displayName}]`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1350,7 +1350,15 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
if (ids.includes(this.currentThreadId)) {
|
||||
return;
|
||||
}
|
||||
await this.client.request("thread/resume", { threadId: this.currentThreadId });
|
||||
const params: Record<string, unknown> = { threadId: this.currentThreadId };
|
||||
if (this.config.systemPrompt?.trim()) {
|
||||
params.developerInstructions = this.config.systemPrompt.trim();
|
||||
}
|
||||
const codexConfig = this.buildCodexInnerConfig();
|
||||
if (codexConfig) {
|
||||
params.config = codexConfig;
|
||||
}
|
||||
await this.client.request("thread/resume", params);
|
||||
} catch (error) {
|
||||
this.logger.warn({ error }, "Failed to resume Codex thread, starting new thread");
|
||||
this.currentThreadId = null;
|
||||
@@ -1438,6 +1446,13 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
if (options?.outputSchema) {
|
||||
params.outputSchema = options.outputSchema;
|
||||
}
|
||||
if (this.config.systemPrompt?.trim()) {
|
||||
params.developerInstructions = this.config.systemPrompt.trim();
|
||||
}
|
||||
const codexConfig = this.buildCodexInnerConfig();
|
||||
if (codexConfig) {
|
||||
params.config = codexConfig;
|
||||
}
|
||||
|
||||
await this.client.request("turn/start", params, TURN_START_TIMEOUT_MS);
|
||||
|
||||
@@ -1707,6 +1722,25 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
const preset = MODE_PRESETS[this.currentMode] ?? MODE_PRESETS[DEFAULT_CODEX_MODE_ID];
|
||||
const approvalPolicy = this.config.approvalPolicy ?? preset.approvalPolicy;
|
||||
const sandbox = this.config.sandboxMode ?? preset.sandbox;
|
||||
const innerConfig = this.buildCodexInnerConfig();
|
||||
const response = (await this.client.request("thread/start", {
|
||||
model,
|
||||
cwd: this.config.cwd ?? null,
|
||||
approvalPolicy,
|
||||
sandbox,
|
||||
...(this.config.systemPrompt?.trim()
|
||||
? { developerInstructions: this.config.systemPrompt.trim() }
|
||||
: {}),
|
||||
...(innerConfig ? { config: innerConfig } : {}),
|
||||
})) as CodexThreadStartResponse;
|
||||
const threadId = response?.thread?.id;
|
||||
if (!threadId) {
|
||||
throw new Error("Codex app-server did not return thread id");
|
||||
}
|
||||
this.currentThreadId = threadId;
|
||||
}
|
||||
|
||||
private buildCodexInnerConfig(): Record<string, unknown> | null {
|
||||
const innerConfig: Record<string, unknown> = {};
|
||||
if (this.config.mcpServers) {
|
||||
const mcpServers: Record<string, CodexMcpServerConfig> = {};
|
||||
@@ -1718,21 +1752,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
if (this.config.extra?.codex) {
|
||||
Object.assign(innerConfig, this.config.extra.codex);
|
||||
}
|
||||
const response = (await this.client.request("thread/start", {
|
||||
model,
|
||||
cwd: this.config.cwd ?? null,
|
||||
approvalPolicy,
|
||||
sandbox,
|
||||
...(this.config.systemPrompt?.trim()
|
||||
? { developerInstructions: this.config.systemPrompt.trim() }
|
||||
: {}),
|
||||
...(Object.keys(innerConfig).length > 0 ? { config: innerConfig } : {}),
|
||||
})) as CodexThreadStartResponse;
|
||||
const threadId = response?.thread?.id;
|
||||
if (!threadId) {
|
||||
throw new Error("Codex app-server did not return thread id");
|
||||
}
|
||||
this.currentThreadId = threadId;
|
||||
return Object.keys(innerConfig).length > 0 ? innerConfig : null;
|
||||
}
|
||||
|
||||
private async buildUserInput(prompt: AgentPromptInput): Promise<unknown[]> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const TOOL_TOKEN_REGEX = /[a-z0-9]+/g;
|
||||
const NAMESPACE_SEPARATOR_REGEX = /[.:/]|__/;
|
||||
const STANDARD_NAMESPACE_SEPARATOR_REGEX = /[.:/]/;
|
||||
|
||||
export function normalizeToolName(name: string): string {
|
||||
return name.trim().toLowerCase();
|
||||
@@ -20,7 +20,23 @@ export function isSpeakToolName(name: string): boolean {
|
||||
}
|
||||
|
||||
export function isLikelyNamespacedToolName(name: string): boolean {
|
||||
return NAMESPACE_SEPARATOR_REGEX.test(normalizeToolName(name));
|
||||
const normalized = normalizeToolName(name);
|
||||
if (STANDARD_NAMESPACE_SEPARATOR_REGEX.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (!normalized.includes("__")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep `__` handling strict to avoid false positives on arbitrary custom names.
|
||||
const segments = normalized.split("__").filter((segment) => segment.length > 0);
|
||||
if (segments.length >= 3) {
|
||||
return true;
|
||||
}
|
||||
if (segments.length === 2 && segments[1]!.includes("_")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isLikelyExternalToolName(name: string): boolean {
|
||||
|
||||
@@ -57,7 +57,7 @@ export function isVoicePermissionAllowed(request: AgentPermissionRequest): boole
|
||||
return false;
|
||||
}
|
||||
|
||||
const mentionsAllowedTooling = containsAny(metadataText, ALLOWED_TOKEN_SET);
|
||||
const mentionsAllowedTooling = containsAny(metadataText, SPEAK_TOKEN_SET);
|
||||
const mentionsDeniedTooling = containsAny(metadataText, DENIED_TOKEN_SET);
|
||||
return mentionsAllowedTooling && !mentionsDeniedTooling;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
stripShellWrapperPrefix,
|
||||
extractPrincipalParam,
|
||||
normalizeToolDisplayName,
|
||||
stripCwdPrefix,
|
||||
} from "./tool-call-parsers.js";
|
||||
|
||||
@@ -90,6 +91,26 @@ describe("extractPrincipalParam", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeToolDisplayName", () => {
|
||||
test("normalizes plain speak name", () => {
|
||||
expect(normalizeToolDisplayName("speak")).toBe("Speak");
|
||||
});
|
||||
|
||||
test("normalizes codex namespaced speak name", () => {
|
||||
expect(normalizeToolDisplayName("paseo_voice.speak")).toBe("Speak");
|
||||
});
|
||||
|
||||
test("normalizes claude mcp speak name", () => {
|
||||
expect(normalizeToolDisplayName("mcp__paseo_voice__speak")).toBe("Speak");
|
||||
});
|
||||
|
||||
test("keeps non-speak names unchanged", () => {
|
||||
expect(normalizeToolDisplayName("paseo__create_agent")).toBe(
|
||||
"paseo__create_agent"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripCwdPrefix", () => {
|
||||
test("strips cwd prefix from file path", () => {
|
||||
expect(stripCwdPrefix("/Users/dev/project/src/file.ts", "/Users/dev/project")).toBe("src/file.ts");
|
||||
|
||||
@@ -63,6 +63,21 @@ export function stripCwdPrefix(filePath: string, cwd?: string): string {
|
||||
// This is used for display purposes to show the actual command being run.
|
||||
const SHELL_WRAPPER_PREFIX_PATTERN = /^\/bin\/(?:zsh|bash|sh)\s+(?:-[a-zA-Z]+\s+)?/;
|
||||
const CD_AND_PATTERN = /^cd\s+(?:"[^"]+"|'[^']+'|\S+)\s+&&\s+/;
|
||||
const TOOL_TOKEN_REGEX = /[a-z0-9]+/g;
|
||||
|
||||
export function normalizeToolDisplayName(toolName: string): string {
|
||||
const normalized = toolName.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return toolName;
|
||||
}
|
||||
|
||||
const tokens = normalized.match(TOOL_TOKEN_REGEX) ?? [];
|
||||
const leaf = tokens[tokens.length - 1];
|
||||
if (leaf === "speak") {
|
||||
return "Speak";
|
||||
}
|
||||
return toolName;
|
||||
}
|
||||
|
||||
export function stripShellWrapperPrefix(command: string): string {
|
||||
const prefixMatch = command.match(SHELL_WRAPPER_PREFIX_PATTERN);
|
||||
|
||||
Reference in New Issue
Block a user