Update files

This commit is contained in:
Mohamed Boudra
2026-02-07 14:36:30 +07:00
parent ed32394acd
commit 5d890ea0b8
12 changed files with 168 additions and 114 deletions
+6 -4
View File
@@ -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": {
+21 -8
View File
@@ -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
View File
@@ -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 -2
View File
@@ -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);