perchai-cli 2.4.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/dist/perch.cjs +339 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,6 +6,7 @@ Run Perch from a terminal without installing the Desktop app.
6
6
  npm install -g perchai-cli
7
7
  perch login --app-url https://app.perchai.app
8
8
  perch status
9
+ perch --cwd ~/Desktop/Project --permission take_the_wheel
9
10
  perch run "Summarize this folder" --cwd .
10
11
  perch ap packet ./01-ap
11
12
  ```
@@ -13,3 +14,22 @@ perch ap packet ./01-ap
13
14
  `perch login` opens your browser, completes OAuth through Perch, and saves the CLI session in the macOS Keychain or `~/.perch/cli-auth-session.json` on other platforms.
14
15
 
15
16
  The CLI uses the hosted Perch model proxy by default when `--app-url` or `PERCH_CLI_APP_URL` points at a Perch app.
17
+
18
+ ## Interactive Commands
19
+
20
+ Run `perch`, then use these commands inside the terminal chat:
21
+
22
+ - `/help` — show commands.
23
+ - `/status` — show cwd, auth, mode, persona, permission, and thread.
24
+ - `/cwd [dir]` — show or change the working directory.
25
+ - `/permission [default|auto_review|take_the_wheel|plan]` — show or change permission mode.
26
+ - `/permissions [default|auto_review|take_the_wheel|plan]` — alias for `/permission`.
27
+ - `/mode [ask|agents|plan]` — show or change chat mode.
28
+ - `/persona [saffron|quill]` — show or change persona.
29
+ - `/app-url [url]` — show or change the Perch app/model proxy URL.
30
+ - `/local-tools [on|off]` — show or toggle local shell/file tools.
31
+ - `/thread [new|id]` — show, reset, or set the current thread id.
32
+ - `/clear` — clear local conversation memory for this terminal session.
33
+ - `/login` — sign in through browser OAuth.
34
+ - `/logout` — clear saved CLI auth.
35
+ - `/exit` — leave Perch CLI.
package/dist/perch.cjs CHANGED
@@ -213565,7 +213565,7 @@ function isSameTurnDuplicateExecution(execution) {
213565
213565
  }
213566
213566
 
213567
213567
  // features/perchTerminal/runtime/toolLoop.ts
213568
- var MAX_ITERATIONS = 10;
213568
+ var MAX_ITERATIONS = 32;
213569
213569
  var MAX_WORKER_ITERATIONS = 32;
213570
213570
  var SOURCE_ANALYSIS_MAX_ITERATIONS = 64;
213571
213571
  var MAX_PARALLEL_TOOL_CALLS = 8;
@@ -215387,7 +215387,7 @@ function addModelUsageSnapshots(current, next) {
215387
215387
  };
215388
215388
  }
215389
215389
  function sanitizeMaxIterations(value, cap = MAX_ITERATIONS) {
215390
- const defaultIterations = cap === MAX_WORKER_ITERATIONS ? 20 : MAX_ITERATIONS;
215390
+ const defaultIterations = cap === MAX_WORKER_ITERATIONS && MAX_WORKER_ITERATIONS > MAX_ITERATIONS ? 20 : MAX_ITERATIONS;
215391
215391
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
215392
215392
  return defaultIterations;
215393
215393
  return Math.min(Math.floor(value), cap);
@@ -221201,7 +221201,7 @@ var DEFAULT_CLI_LOGIN_APP_URL = "https://app.perchai.app";
221201
221201
  var HELP_TEXT = `Perch CLI
221202
221202
 
221203
221203
  Usage:
221204
- perch [--app-url <url>]
221204
+ perch [--cwd <dir>] [--permission default|auto_review|take_the_wheel|plan] [--mode ask|agents|plan] [--persona saffron|quill] [--app-url <url>]
221205
221205
  perch login [--app-url <url>]
221206
221206
  perch status
221207
221207
  perch logout
@@ -221224,6 +221224,22 @@ Model connection:
221224
221224
  --app-url Send model calls through a running Perch app, e.g. http://localhost:3000.
221225
221225
  You can also set PERCH_CLI_APP_URL.
221226
221226
  `;
221227
+ var INTERACTIVE_HELP_TEXT = `Interactive commands:
221228
+ /help Show this list.
221229
+ /status Show cwd, auth, mode, persona, permission, and thread.
221230
+ /cwd [dir] Show or change the working directory.
221231
+ /permission [mode] Show or set default, auto_review, take_the_wheel, or plan.
221232
+ /permissions [mode] Alias for /permission.
221233
+ /mode [ask|agents|plan] Show or set chat mode.
221234
+ /persona [saffron|quill] Show or set persona.
221235
+ /app-url [url] Show or change the Perch app/model proxy URL.
221236
+ /local-tools [on|off] Show or toggle local shell/file tools.
221237
+ /thread [new|id] Show, reset, or set the current thread id.
221238
+ /clear Clear local conversation memory for this terminal session.
221239
+ /login Sign in through browser OAuth.
221240
+ /logout Clear saved CLI auth.
221241
+ /exit Leave Perch CLI.
221242
+ `;
221227
221243
  async function runPerchCli(argv, writer = defaultWriter(), deps = {}) {
221228
221244
  const parsed = parsePerchCli(argv);
221229
221245
  if (!parsed.ok) {
@@ -221237,7 +221253,7 @@ ${HELP_TEXT}`);
221237
221253
  return 0;
221238
221254
  }
221239
221255
  if ("interactive" in parsed) {
221240
- return runInteractivePerchCli(writer, deps, parsed.appUrl);
221256
+ return runInteractivePerchCli(writer, deps, parsed);
221241
221257
  }
221242
221258
  try {
221243
221259
  if (parsed.domain === "auth") {
@@ -221306,6 +221322,7 @@ function parsePerchCli(argv) {
221306
221322
  if (args.length === 0) {
221307
221323
  return global2.appUrl ? { ok: true, interactive: true, appUrl: global2.appUrl } : { ok: true, interactive: true };
221308
221324
  }
221325
+ if (args[0]?.startsWith("--")) return parseInteractiveCommand(args, global2.appUrl);
221309
221326
  const [domain, action, folderPath, ...rest2] = args;
221310
221327
  if (domain === "run") return parseRunCommand(args.slice(1), global2.appUrl);
221311
221328
  if (domain === "login") return { ok: true, domain: "auth", action: "login", ...global2.appUrl ? { appUrl: global2.appUrl } : {} };
@@ -221434,7 +221451,84 @@ function parseRunCommand(rest2, inheritedAppUrl) {
221434
221451
  ...appUrl ? { appUrl } : {}
221435
221452
  };
221436
221453
  }
221437
- async function runInteractivePerchCli(writer, deps, appUrl) {
221454
+ function parseInteractiveCommand(rest2, inheritedAppUrl) {
221455
+ let cwd;
221456
+ let chatMode;
221457
+ let personaId;
221458
+ let permissionMode;
221459
+ let threadId;
221460
+ let desktopConnected;
221461
+ let cliLocalTools;
221462
+ let appUrl = inheritedAppUrl;
221463
+ for (let index = 0; index < rest2.length; index++) {
221464
+ const value = rest2[index];
221465
+ if (value === "--desktop-tools") {
221466
+ desktopConnected = true;
221467
+ continue;
221468
+ }
221469
+ if (value === "--no-local-tools") {
221470
+ cliLocalTools = false;
221471
+ continue;
221472
+ }
221473
+ if (value === "--local-tools") {
221474
+ cliLocalTools = true;
221475
+ continue;
221476
+ }
221477
+ if (value === "--app-url") {
221478
+ appUrl = rest2[index + 1];
221479
+ index += 1;
221480
+ if (!appUrl) return { ok: false, error: "--app-url requires a URL" };
221481
+ continue;
221482
+ }
221483
+ if (value === "--cwd") {
221484
+ cwd = rest2[index + 1];
221485
+ index += 1;
221486
+ if (!cwd) return { ok: false, error: "--cwd requires a directory path" };
221487
+ continue;
221488
+ }
221489
+ if (value === "--mode") {
221490
+ const next = rest2[index + 1];
221491
+ index += 1;
221492
+ if (!isChatMode(next)) return { ok: false, error: `Unknown chat mode: ${next ?? "(missing)"}` };
221493
+ chatMode = next;
221494
+ continue;
221495
+ }
221496
+ if (value === "--persona") {
221497
+ const next = rest2[index + 1];
221498
+ index += 1;
221499
+ if (!isPersonaId(next)) return { ok: false, error: `Unknown persona: ${next ?? "(missing)"}` };
221500
+ personaId = next;
221501
+ continue;
221502
+ }
221503
+ if (value === "--permission") {
221504
+ const next = rest2[index + 1];
221505
+ index += 1;
221506
+ if (!isPermissionMode(next)) return { ok: false, error: `Unknown permission mode: ${next ?? "(missing)"}` };
221507
+ permissionMode = next;
221508
+ continue;
221509
+ }
221510
+ if (value === "--thread") {
221511
+ threadId = rest2[index + 1];
221512
+ index += 1;
221513
+ if (!threadId) return { ok: false, error: "--thread requires an id" };
221514
+ continue;
221515
+ }
221516
+ return { ok: false, error: `Unknown interactive flag: ${value}` };
221517
+ }
221518
+ return {
221519
+ ok: true,
221520
+ interactive: true,
221521
+ ...appUrl ? { appUrl } : {},
221522
+ ...cwd ? { cwd } : {},
221523
+ ...chatMode ? { chatMode } : {},
221524
+ ...personaId ? { personaId } : {},
221525
+ ...permissionMode ? { permissionMode } : {},
221526
+ ...threadId ? { threadId } : {},
221527
+ ...desktopConnected !== void 0 ? { desktopConnected } : {},
221528
+ ...cliLocalTools !== void 0 ? { cliLocalTools } : {}
221529
+ };
221530
+ }
221531
+ async function runInteractivePerchCli(writer, deps, options) {
221438
221532
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
221439
221533
  writer.stdout(HELP_TEXT);
221440
221534
  return 0;
@@ -221444,33 +221538,65 @@ async function runInteractivePerchCli(writer, deps, appUrl) {
221444
221538
  output: process.stdout,
221445
221539
  terminal: true
221446
221540
  });
221447
- const threadId = `cli-${Date.now()}`;
221448
- const recentMessages = [];
221449
- const connection = await connectCliModelProxy({ appUrl });
221450
- writer.stdout("Perch CLI chat. Type /exit to quit.\n");
221541
+ const state = {
221542
+ cwd: resolveExistingDirectory(options.cwd ?? process.cwd()),
221543
+ chatMode: options.chatMode ?? "agents",
221544
+ personaId: options.personaId ?? "saffron",
221545
+ permissionMode: options.permissionMode ?? "default",
221546
+ threadId: options.threadId?.trim() || `cli-${Date.now()}`,
221547
+ desktopConnected: options.desktopConnected ?? false,
221548
+ cliLocalTools: options.cliLocalTools ?? true,
221549
+ appUrl: resolveCliAppUrl(options.appUrl ?? null, null),
221550
+ recentMessages: []
221551
+ };
221552
+ let connection = await connectCliModelProxy({ appUrl: state.appUrl });
221553
+ if (!state.appUrl && connection.appUrl) state.appUrl = connection.appUrl;
221554
+ const reconnect = async () => {
221555
+ connection.restore();
221556
+ connection = await connectCliModelProxy({ appUrl: state.appUrl });
221557
+ if (!state.appUrl && connection.appUrl) state.appUrl = connection.appUrl;
221558
+ };
221559
+ writer.stdout("Perch CLI chat. Type /help for commands, /exit to quit.\n");
221560
+ writer.stdout(renderInteractiveStatus(state, connection));
221451
221561
  try {
221452
221562
  while (true) {
221453
221563
  const prompt = (await rl.question("perch> ")).trim();
221454
221564
  if (!prompt) continue;
221455
221565
  if (prompt === "/exit" || prompt === "exit" || prompt === "quit") break;
221566
+ if (prompt.startsWith("/")) {
221567
+ try {
221568
+ const commandResult = await runInteractiveSlashCommand({
221569
+ input: prompt,
221570
+ state,
221571
+ writer,
221572
+ reconnect
221573
+ });
221574
+ if (commandResult === "exit") break;
221575
+ } catch (error) {
221576
+ const message = error instanceof Error ? error.message : String(error);
221577
+ writer.stderr(`${message}
221578
+ `);
221579
+ }
221580
+ continue;
221581
+ }
221456
221582
  const result2 = await (deps.runCliTurn ?? runPerchCliTurn)({
221457
221583
  prompt,
221458
- cwd: process.cwd(),
221459
- chatMode: "agents",
221460
- personaId: "saffron",
221461
- permissionMode: "default",
221462
- threadId,
221463
- recentMessages,
221584
+ cwd: state.cwd,
221585
+ chatMode: state.chatMode,
221586
+ personaId: state.personaId,
221587
+ permissionMode: state.permissionMode,
221588
+ threadId: state.threadId,
221589
+ recentMessages: state.recentMessages,
221464
221590
  founderModelSelection: connection.founderModelSelection,
221465
- desktopConnected: false,
221466
- cliLocalTools: true
221591
+ desktopConnected: state.desktopConnected,
221592
+ cliLocalTools: state.cliLocalTools
221467
221593
  });
221468
221594
  writeCliTurnResult(result2, false, writer);
221469
- appendRecentMessage(recentMessages, "user", prompt);
221595
+ appendRecentMessage(state.recentMessages, "user", prompt);
221470
221596
  if (result2.assistantText.trim()) {
221471
- appendRecentMessage(recentMessages, "assistant", result2.assistantText.trim());
221597
+ appendRecentMessage(state.recentMessages, "assistant", result2.assistantText.trim());
221472
221598
  }
221473
- if (recentMessages.length > 30) recentMessages.splice(0, recentMessages.length - 30);
221599
+ if (state.recentMessages.length > 30) state.recentMessages.splice(0, state.recentMessages.length - 30);
221474
221600
  }
221475
221601
  return 0;
221476
221602
  } catch (error) {
@@ -221482,6 +221608,179 @@ async function runInteractivePerchCli(writer, deps, appUrl) {
221482
221608
  rl.close();
221483
221609
  }
221484
221610
  }
221611
+ async function runInteractiveSlashCommand(input) {
221612
+ const parsed = parseInteractiveSlashCommand(input.input);
221613
+ if (!parsed.ok) {
221614
+ input.writer.stderr(`${parsed.error}
221615
+ `);
221616
+ return "continue";
221617
+ }
221618
+ switch (parsed.kind) {
221619
+ case "help":
221620
+ input.writer.stdout(INTERACTIVE_HELP_TEXT);
221621
+ return "continue";
221622
+ case "status": {
221623
+ const session = await readStoredCliAuthSession();
221624
+ input.writer.stdout(renderInteractiveStatus(input.state, null, session));
221625
+ return "continue";
221626
+ }
221627
+ case "cwd":
221628
+ if (!parsed.value) {
221629
+ input.writer.stdout(`cwd: ${input.state.cwd}
221630
+ `);
221631
+ return "continue";
221632
+ }
221633
+ input.state.cwd = resolveExistingDirectory(parsed.value);
221634
+ input.writer.stdout(`cwd: ${input.state.cwd}
221635
+ `);
221636
+ return "continue";
221637
+ case "permission":
221638
+ if (!parsed.value) {
221639
+ input.writer.stdout(`permission: ${input.state.permissionMode}
221640
+ `);
221641
+ return "continue";
221642
+ }
221643
+ input.state.permissionMode = parsed.value;
221644
+ input.writer.stdout(`permission: ${input.state.permissionMode}
221645
+ `);
221646
+ return "continue";
221647
+ case "mode":
221648
+ if (!parsed.value) {
221649
+ input.writer.stdout(`mode: ${input.state.chatMode}
221650
+ `);
221651
+ return "continue";
221652
+ }
221653
+ input.state.chatMode = parsed.value;
221654
+ input.writer.stdout(`mode: ${input.state.chatMode}
221655
+ `);
221656
+ return "continue";
221657
+ case "persona":
221658
+ if (!parsed.value) {
221659
+ input.writer.stdout(`persona: ${input.state.personaId}
221660
+ `);
221661
+ return "continue";
221662
+ }
221663
+ input.state.personaId = parsed.value;
221664
+ input.writer.stdout(`persona: ${input.state.personaId}
221665
+ `);
221666
+ return "continue";
221667
+ case "appUrl":
221668
+ if (!parsed.value) {
221669
+ input.writer.stdout(`app-url: ${input.state.appUrl ?? "(none)"}
221670
+ `);
221671
+ return "continue";
221672
+ }
221673
+ input.state.appUrl = requireCliAppUrl(parsed.value);
221674
+ await input.reconnect();
221675
+ input.writer.stdout(`app-url: ${input.state.appUrl}
221676
+ `);
221677
+ return "continue";
221678
+ case "localTools":
221679
+ if (parsed.value === void 0) {
221680
+ input.writer.stdout(`local-tools: ${input.state.cliLocalTools ? "on" : "off"}
221681
+ `);
221682
+ return "continue";
221683
+ }
221684
+ input.state.cliLocalTools = parsed.value;
221685
+ input.writer.stdout(`local-tools: ${input.state.cliLocalTools ? "on" : "off"}
221686
+ `);
221687
+ return "continue";
221688
+ case "thread":
221689
+ if (!parsed.value) {
221690
+ input.writer.stdout(`thread: ${input.state.threadId}
221691
+ `);
221692
+ return "continue";
221693
+ }
221694
+ input.state.threadId = parsed.value === "new" ? `cli-${Date.now()}` : parsed.value;
221695
+ input.state.recentMessages = [];
221696
+ input.writer.stdout(`thread: ${input.state.threadId}
221697
+ `);
221698
+ return "continue";
221699
+ case "clear":
221700
+ input.state.recentMessages = [];
221701
+ input.writer.stdout("Conversation memory cleared for this terminal session.\n");
221702
+ return "continue";
221703
+ case "login":
221704
+ await runAuthCommand({
221705
+ ok: true,
221706
+ domain: "auth",
221707
+ action: "login",
221708
+ ...input.state.appUrl ? { appUrl: input.state.appUrl } : {}
221709
+ }, input.writer);
221710
+ await input.reconnect();
221711
+ return "continue";
221712
+ case "logout":
221713
+ await clearStoredCliAuthSession();
221714
+ await input.reconnect();
221715
+ input.writer.stdout("Perch CLI session cleared.\n");
221716
+ return "continue";
221717
+ case "exit":
221718
+ return "exit";
221719
+ }
221720
+ }
221721
+ function parseInteractiveSlashCommand(input) {
221722
+ const trimmed = input.trim();
221723
+ if (!trimmed.startsWith("/")) return { ok: false, error: "Slash commands must start with /." };
221724
+ const [rawCommand, ...parts] = trimmed.slice(1).split(/\s+/).filter(Boolean);
221725
+ const command = rawCommand?.toLowerCase();
221726
+ const value = parts.join(" ").trim() || void 0;
221727
+ switch (command) {
221728
+ case "help":
221729
+ case "?":
221730
+ return { ok: true, kind: "help" };
221731
+ case "status":
221732
+ return { ok: true, kind: "status" };
221733
+ case "cwd":
221734
+ return { ok: true, kind: "cwd", ...value ? { value } : {} };
221735
+ case "permission":
221736
+ case "permissions":
221737
+ if (!value) return { ok: true, kind: "permission" };
221738
+ if (!isPermissionMode(value)) return { ok: false, error: `Unknown permission mode: ${value}` };
221739
+ return { ok: true, kind: "permission", value };
221740
+ case "mode":
221741
+ if (!value) return { ok: true, kind: "mode" };
221742
+ if (!isChatMode(value)) return { ok: false, error: `Unknown chat mode: ${value}` };
221743
+ return { ok: true, kind: "mode", value };
221744
+ case "persona":
221745
+ if (!value) return { ok: true, kind: "persona" };
221746
+ if (!isPersonaId(value)) return { ok: false, error: `Unknown persona: ${value}` };
221747
+ return { ok: true, kind: "persona", value };
221748
+ case "app-url":
221749
+ case "app":
221750
+ return { ok: true, kind: "appUrl", ...value ? { value } : {} };
221751
+ case "local-tools":
221752
+ case "tools":
221753
+ if (!value) return { ok: true, kind: "localTools" };
221754
+ if (value !== "on" && value !== "off") return { ok: false, error: "/local-tools expects on or off." };
221755
+ return { ok: true, kind: "localTools", value: value === "on" };
221756
+ case "thread":
221757
+ return { ok: true, kind: "thread", ...value ? { value: value === "new" ? "new" : value } : {} };
221758
+ case "clear":
221759
+ return { ok: true, kind: "clear" };
221760
+ case "login":
221761
+ return { ok: true, kind: "login" };
221762
+ case "logout":
221763
+ return { ok: true, kind: "logout" };
221764
+ case "exit":
221765
+ case "quit":
221766
+ return { ok: true, kind: "exit" };
221767
+ default:
221768
+ return { ok: false, error: `Unknown command: /${rawCommand ?? ""}. Type /help for commands.` };
221769
+ }
221770
+ }
221771
+ function renderInteractiveStatus(state, connection, session) {
221772
+ const auth = session === void 0 ? connection?.authenticated ? `signed in${connection.email ? ` as ${connection.email}` : ""}` : "public/anonymous" : isStoredCliAuthSessionUsable(session) ? `signed in${session.email ? ` as ${session.email}` : ""}` : "not signed in";
221773
+ return [
221774
+ `cwd: ${state.cwd}`,
221775
+ `permission: ${state.permissionMode}`,
221776
+ `mode: ${state.chatMode}`,
221777
+ `persona: ${state.personaId}`,
221778
+ `thread: ${state.threadId}`,
221779
+ `local-tools: ${state.cliLocalTools ? "on" : "off"}`,
221780
+ `app-url: ${state.appUrl ?? connection?.appUrl ?? "(none)"}`,
221781
+ `auth: ${auth}`
221782
+ ].join(" \xB7 ") + "\n";
221783
+ }
221485
221784
  function parseGlobalFlags(argv) {
221486
221785
  const args = [];
221487
221786
  let appUrl;
@@ -221676,11 +221975,31 @@ function resolveFolderPath(input) {
221676
221975
  if (!stat?.isDirectory()) throw new Error(`Folder does not exist: ${resolved}`);
221677
221976
  return resolved;
221678
221977
  }
221978
+ function resolveExistingDirectory(input) {
221979
+ const resolved = resolvePath(input);
221980
+ const stat = import_node_fs10.default.existsSync(resolved) ? import_node_fs10.default.statSync(resolved) : null;
221981
+ if (!stat?.isDirectory()) throw new Error(`Directory does not exist: ${resolved}`);
221982
+ return resolved;
221983
+ }
221679
221984
  function resolvePath(input) {
221680
221985
  if (input === "~") return process.env.HOME ?? input;
221681
221986
  if (input.startsWith("~/")) return import_node_path15.default.join(process.env.HOME ?? "", input.slice(2));
221682
221987
  return import_node_path15.default.resolve(input);
221683
221988
  }
221989
+ function requireCliAppUrl(input) {
221990
+ const resolved = resolveCliAppUrl(input, null);
221991
+ if (!resolved) throw new Error(`Invalid app URL: ${input}`);
221992
+ return resolved;
221993
+ }
221994
+ function isChatMode(value) {
221995
+ return value === "ask" || value === "agents" || value === "plan";
221996
+ }
221997
+ function isPersonaId(value) {
221998
+ return value === "saffron" || value === "quill";
221999
+ }
222000
+ function isPermissionMode(value) {
222001
+ return value === "default" || value === "auto_review" || value === "take_the_wheel" || value === "plan";
222002
+ }
221684
222003
  function formatMoney2(value) {
221685
222004
  return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
221686
222005
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perchai-cli",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "Perch AI command-line interface",
5
5
  "bin": {
6
6
  "perch": "bin/perch"