perchai-cli 2.4.20 → 2.4.21

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 (2) hide show
  1. package/dist/perch.mjs +281 -103
  2. package/package.json +1 -1
package/dist/perch.mjs CHANGED
@@ -78260,7 +78260,7 @@ var init_permissionModes = __esm({
78260
78260
  });
78261
78261
 
78262
78262
  // features/perchTerminal/runtime/perchMemoryGuidance.ts
78263
- var VISIBLE_OUTPUT_STYLE_GUIDANCE, SAFFRON_CORE_IDENTITY, QUILL_CORE_IDENTITY, PERCH_MEMORY_GUIDANCE;
78263
+ var VISIBLE_OUTPUT_STYLE_GUIDANCE, SAFFRON_STYLE_CONTRACT, QUILL_STYLE_CONTRACT, SAFFRON_CORE_IDENTITY, QUILL_CORE_IDENTITY, PERCH_MEMORY_GUIDANCE;
78264
78264
  var init_perchMemoryGuidance = __esm({
78265
78265
  "features/perchTerminal/runtime/perchMemoryGuidance.ts"() {
78266
78266
  "use strict";
@@ -78275,6 +78275,31 @@ Avoid stock AI phrases: "delve," "nuanced," "robust," "seamless,"
78275
78275
  Be concise when the user is moving fast.
78276
78276
  Preserve the persona: Quill is warm, literate, and direct; Saffron is sharp,
78277
78277
  practical, and confident.
78278
+ `.trim();
78279
+ SAFFRON_STYLE_CONTRACT = `
78280
+ ## Voice contract \u2014 Saffron (visible output only)
78281
+
78282
+ This contract shapes how your replies read. It never changes which tools you
78283
+ use, which tasks you take on, or how the work itself is done.
78284
+
78285
+ - Direct, sharp, practical, operator-like, confident.
78286
+ - Lead with the finding or the action. Decoration is at most a dry aside.
78287
+ - Memory texture: use remembered facts operationally ("your close lands on the
78288
+ 5th, so this ships before it") \u2014 state the fact, use it, move on.
78289
+ - Concise. Confidence reads as economy, not volume.
78290
+ `.trim();
78291
+ QUILL_STYLE_CONTRACT = `
78292
+ ## Voice contract \u2014 Quill (visible output only)
78293
+
78294
+ This contract shapes how your replies read. It never changes which tools you
78295
+ use, which tasks you take on, or how the work itself is done.
78296
+
78297
+ - Warm, personable, literate, more emotionally present than a status report.
78298
+ - Lightly humorous when it arises naturally; never forced, never a bit.
78299
+ - Memory texture: when relevant personal memories are in context, weave them in
78300
+ naturally ("you mentioned the board reads these, so I kept it tight").
78301
+ - Still concise and useful. Warmth lives in the engagement, not the word count.
78302
+ - Hard limits: not verbose, not therapeutic, not flirty, not over-familiar.
78278
78303
  `.trim();
78279
78304
  SAFFRON_CORE_IDENTITY = `
78280
78305
  You are Saffron \u2014 Perch's workspace operator.
@@ -78411,6 +78436,11 @@ working through a paper. Founders tightening a pitch. Anyone who wants their
78411
78436
  words to land. You don't try to write *for* people; you help them write
78412
78437
  *better*. You know the difference, and you respect the difference.
78413
78438
 
78439
+ Writing is your craft home, not your fence. You're a full Perch operator: when
78440
+ the user needs analysis, files, data, an audit, or delivery, you do that work
78441
+ yourself with the same tools \u2014 in your own voice. You never punt real work to
78442
+ another persona.
78443
+
78414
78444
  ## How you work
78415
78445
 
78416
78446
  Read first. Before you draft a single sentence, you know what the piece is for,
@@ -80878,6 +80908,7 @@ var init_personaRegistry = __esm({
80878
80908
  label: "Saffron",
80879
80909
  shortDescription: "Operator. Direct, sharp, finds the thing.",
80880
80910
  identityPrompt: SAFFRON_CORE_IDENTITY,
80911
+ styleContract: SAFFRON_STYLE_CONTRACT,
80881
80912
  suggestedPrompts: [
80882
80913
  "Audit the AP folder for duplicates and anomalies",
80883
80914
  "Reconcile the GL against last month's subledger",
@@ -80901,6 +80932,7 @@ var init_personaRegistry = __esm({
80901
80932
  label: "Quill",
80902
80933
  shortDescription: "Writing companion. Warm, craft-aware, reads first.",
80903
80934
  identityPrompt: QUILL_CORE_IDENTITY,
80935
+ styleContract: QUILL_STYLE_CONTRACT,
80904
80936
  suggestedPrompts: [
80905
80937
  "Draft a memo on personal jurisdiction",
80906
80938
  "Brief Pennoyer v. Neff",
@@ -85443,9 +85475,9 @@ var QUILL_SPECIALIST_AGENTS_PROMPT;
85443
85475
  var init_quillSpecialistPrompt = __esm({
85444
85476
  "features/perchTerminal/agentPlatform/quillSpecialistPrompt.ts"() {
85445
85477
  QUILL_SPECIALIST_AGENTS_PROMPT = `
85446
- ## Quill \u2014 Specialist Delegation
85478
+ ## Writing & Research \u2014 Specialist Delegation
85447
85479
 
85448
- You are Quill, the writing persona. You do not run playbooks or suites. You write and deliver in your own loop, dispatching specialists ONLY for research/verification fan-out \u2014 never for delivery.
85480
+ This task is writing/research/legal shaped. You write and deliver in your own loop, dispatching specialists ONLY for research/verification fan-out \u2014 never for delivery.
85449
85481
 
85450
85482
  Use specialists (dispatch_agent) when independent research or verification benefits from separation:
85451
85483
  - general_writer: drafts polished letters, memos, essays, emails, and revisions from the user's request plus provided context. It does NOT research \u2014 only give it general_writer work once you already have the sources/facts it needs.
@@ -85483,7 +85515,6 @@ Success rules:
85483
85515
  - Do not say a Google Doc was created or an email was sent unless you have a verified receipt (URL / sent confirmation) or a screenshot showing the result.
85484
85516
  - Safety: never click anything that grants access or changes sharing/permissions, such as "Share & send", "Share", or "Grant access". If a share/permission dialog appears, choose the option that sends without changing permissions, such as "Send without sharing" or "Send anyway".
85485
85517
  - If delivery hits a problem, recover in the same loop. Try the next reasonable path, or ask the user for permission/choice; do not silently stop or skip the requested delivery. Never claim success without proof.
85486
- - If the user asks for finance, AP, payroll, KYC, market, meeting, or close work, suggest Saffron unless the task is purely writing the prose.
85487
85518
  `.trim();
85488
85519
  }
85489
85520
  });
@@ -91884,6 +91915,7 @@ function buildCoreSystemSection(input) {
91884
91915
  ].join("\n")
91885
91916
  );
91886
91917
  }
91918
+ lines.push("", persona.styleContract);
91887
91919
  return lines.join("\n");
91888
91920
  }
91889
91921
  function appendAgentsModeGuidance(input) {
@@ -91916,9 +91948,8 @@ function appendAgentsModeGuidance(input) {
91916
91948
  });
91917
91949
  return;
91918
91950
  }
91919
- const isQuill = personaId === "quill";
91920
91951
  lines.push(
91921
- isQuill ? "You're in Quill mode. Get writing, research, legal, and delivery work done by orchestrating specialist sub-agents, not by running suites or playbooks." : "You're in Agents mode. Get the work done.",
91952
+ "You're in Agents mode. Get the work done.",
91922
91953
  "Execute directly when the task is clear and bounded.",
91923
91954
  "For greetings, thanks, quick check-ins, or explanation-only questions, answer directly without tools unless the user asks you to inspect, search, run, create, send, or change something.",
91924
91955
  "For delivery, write/send/change, filesystem, browser, or other external action turns, call the appropriate tool in the same turn and never end with only a promise, preamble, apology, or status line. For chat-only drafts, summaries, opinions, or answers, once evidence is available, synthesize directly in chat and stop.",
@@ -91938,9 +91969,10 @@ function appendAgentsModeGuidance(input) {
91938
91969
  if (approvedPlanBlock) {
91939
91970
  lines.push("", approvedPlanBlock);
91940
91971
  }
91941
- if (isQuill) {
91972
+ if (personaId === "quill" && isWritingResearchIntent(assemblyInput.trimmedInput)) {
91942
91973
  lines.push("", QUILL_SPECIALIST_AGENTS_PROMPT);
91943
- } else if (assemblyInput.coordinatorMode || isFinancialOperatorIntent(assemblyInput.trimmedInput)) {
91974
+ }
91975
+ if (assemblyInput.coordinatorMode || isFinancialOperatorIntent(assemblyInput.trimmedInput)) {
91944
91976
  lines.push("", FINANCIAL_OPERATOR_AGENTS_PROMPT);
91945
91977
  }
91946
91978
  lines.push("", PLATFORM_DELIVERY_GUIDANCE);
@@ -92053,6 +92085,11 @@ function isFinancialOperatorIntent(input) {
92053
92085
  input
92054
92086
  );
92055
92087
  }
92088
+ function isWritingResearchIntent(input) {
92089
+ return /\b(writ(?:e|ing|ten)|draft|redraft|rewrite|revise|edit|proofread|polish|memo(?:randum)?|essay|letter|brief|motion|paper|article|blog|post|thesis|abstract|summar(?:y|ize|ise)|cit(?:e|ation|ations)|research|sources?|irac|case[-\s]?law|statute|regulation|legal|law\s+review)\b/i.test(
92090
+ input
92091
+ );
92092
+ }
92056
92093
  function buildApprovedGeneralPlanBlock(session) {
92057
92094
  const approvedPlan = session?.approvedGeneralPlan;
92058
92095
  if (!approvedPlan) return null;
@@ -199912,6 +199949,7 @@ function containsBrowserDeliveryTask(tasks) {
199912
199949
  var BROWSER_DELIVERY_ROLE_IDS;
199913
199950
  var init_browserDeliveryLock = __esm({
199914
199951
  "features/perchTerminal/agentPlatform/browserDeliveryLock.ts"() {
199952
+ "use strict";
199915
199953
  BROWSER_DELIVERY_ROLE_IDS = /* @__PURE__ */ new Set([
199916
199954
  "doc_writer",
199917
199955
  "email_sender",
@@ -202537,8 +202575,6 @@ async function dispatchAgentHandler(args, ctx) {
202537
202575
  parentToolCallId: ctx.parentToolCallId,
202538
202576
  mcpTools: ctx.mcpTools ?? []
202539
202577
  };
202540
- const isQuillNormalTurn = ctx.personaId === "quill" && ctx.chatMode !== "coordinator" && !ctx.allowedCallableAgents?.length;
202541
- void isQuillNormalTurn;
202542
202578
  if (Array.isArray(args.tasks) && args.tasks.length > 0) {
202543
202579
  const tasks = args.tasks.map(
202544
202580
  (t) => ({
@@ -207292,7 +207328,7 @@ var init_sendWorkerMessage2 = __esm({
207292
207328
  });
207293
207329
 
207294
207330
  // features/perchTerminal/runtime/toolSystem/tools/workers/spawnWorker.ts
207295
- var QUILL_BLOCKED_DELIVERY_WORKER_IDS, spawnWorkerTool;
207331
+ var spawnWorkerTool;
207296
207332
  var init_spawnWorker2 = __esm({
207297
207333
  "features/perchTerminal/runtime/toolSystem/tools/workers/spawnWorker.ts"() {
207298
207334
  "use strict";
@@ -207301,11 +207337,6 @@ var init_spawnWorker2 = __esm({
207301
207337
  init_agentDispatch();
207302
207338
  init_localScope();
207303
207339
  init_toolNames();
207304
- QUILL_BLOCKED_DELIVERY_WORKER_IDS = /* @__PURE__ */ new Set([
207305
- "doc_writer",
207306
- "email_sender",
207307
- "calendar_scheduler"
207308
- ]);
207309
207340
  spawnWorkerTool = {
207310
207341
  name: TOOL_NAMES.spawnWorker,
207311
207342
  classification: { native: false },
@@ -207323,8 +207354,6 @@ var init_spawnWorker2 = __esm({
207323
207354
  errorCode: "worker_event_sink_missing"
207324
207355
  };
207325
207356
  }
207326
- if (ctx.personaId === "quill" && ctx.chatMode !== "coordinator" && !ctx.allowedCallableAgents?.length && QUILL_BLOCKED_DELIVERY_WORKER_IDS.has(workerId)) {
207327
- }
207328
207357
  const enrichedContext = await threadPriorSpecialistContext({
207329
207358
  threadId: ctx.threadId,
207330
207359
  roleId: workerId,
@@ -220487,52 +220516,15 @@ var init_toolLoop = __esm({
220487
220516
  }
220488
220517
  });
220489
220518
 
220490
- // features/perchTerminal/runtime/personas/quillToolPolicy.ts
220491
- function isQuillBlockedToolName(toolName) {
220492
- return QUILL_BLOCKED_TOOL_NAMES.has(toolName);
220493
- }
220494
- function filterToolsForQuill(toolDefinitions) {
220495
- return toolDefinitions.filter((tool) => !isQuillBlockedToolName(tool.function.name));
220496
- }
220497
- var QUILL_BLOCKED_TOOL_NAMES;
220498
- var init_quillToolPolicy = __esm({
220499
- "features/perchTerminal/runtime/personas/quillToolPolicy.ts"() {
220500
- "use strict";
220501
- init_toolNames();
220502
- QUILL_BLOCKED_TOOL_NAMES = /* @__PURE__ */ new Set([
220503
- TOOL_NAMES.runSuite,
220504
- TOOL_NAMES.runManagedPlaybook,
220505
- TOOL_NAMES.listSuiteCatalog,
220506
- TOOL_NAMES.proposeSuitePlan,
220507
- TOOL_NAMES.executeSuitePlan,
220508
- TOOL_NAMES.proposeWork,
220509
- TOOL_NAMES.executeWork,
220510
- TOOL_NAMES.generateAPAuditPacket,
220511
- TOOL_NAMES.safeBrowserAction,
220512
- // Deprecated non-verified shortcuts — superseded by the verified surface tools.
220513
- TOOL_NAMES.gmailSendEmail,
220514
- TOOL_NAMES.gmailSaveDraft,
220515
- TOOL_NAMES.googleDocsCreate,
220516
- TOOL_NAMES.googleDocsAppend,
220517
- TOOL_NAMES.googleCalendarCreateEvent,
220518
- TOOL_NAMES.googleSheetsCreate,
220519
- TOOL_NAMES.googleSheetsAppendRows
220520
- ]);
220521
- }
220522
- });
220523
-
220524
- // features/perchTerminal/runtime/personas/saffronToolPolicy.ts
220519
+ // features/perchTerminal/runtime/personas/sharedToolPolicy.ts
220525
220520
  function filterSuiteRelayTools(toolDefinitions, opts = {}) {
220526
220521
  if (opts.allowSuiteRelay) return toolDefinitions;
220527
220522
  return toolDefinitions.filter(
220528
220523
  (tool) => !SUITE_RELAY_TOOL_NAMES.has(tool.function.name)
220529
220524
  );
220530
220525
  }
220531
- function filterToolsForSaffron(toolDefinitions, opts = {}) {
220532
- return filterSuiteRelayTools(toolDefinitions, opts);
220533
- }
220534
- var init_saffronToolPolicy = __esm({
220535
- "features/perchTerminal/runtime/personas/saffronToolPolicy.ts"() {
220526
+ var init_sharedToolPolicy = __esm({
220527
+ "features/perchTerminal/runtime/personas/sharedToolPolicy.ts"() {
220536
220528
  "use strict";
220537
220529
  init_suiteRelayKillSwitch();
220538
220530
  }
@@ -220650,10 +220642,7 @@ async function runLiveAgentsLoop(input) {
220650
220642
  PLAN_MODE_ALLOWED_TOOL_NAMES
220651
220643
  ) : effectiveChatMode === "ask" ? [] : getReadOnlyToolDefinitions(toolOpts);
220652
220644
  const suiteRelayEnabled = isSuiteRelayEnabled();
220653
- const personaFilteredTools = turn.personaId === "quill" ? filterToolsForQuill(baseAgentsTools) : turn.personaId === "saffron" ? filterToolsForSaffron(baseAgentsTools, {
220654
- allowSuiteRelay: suiteRelayEnabled
220655
- }) : baseAgentsTools;
220656
- const suiteRelayFilteredTools = filterSuiteRelayTools(personaFilteredTools, {
220645
+ const suiteRelayFilteredTools = filterSuiteRelayTools(baseAgentsTools, {
220657
220646
  allowSuiteRelay: suiteRelayEnabled
220658
220647
  });
220659
220648
  const deliveryPolicyDeliveryOperatorOnly = turn.deliveryOperatorOnly === true;
@@ -221106,8 +221095,7 @@ var init_liveAgentsLoop = __esm({
221106
221095
  init_toolNames();
221107
221096
  init_planModeStateMachine();
221108
221097
  init_toolPermissionPolicy();
221109
- init_quillToolPolicy();
221110
- init_saffronToolPolicy();
221098
+ init_sharedToolPolicy();
221111
221099
  init_deliveryToolPolicy();
221112
221100
  init_threadSession();
221113
221101
  init_sandboxProvenance();
@@ -225245,10 +225233,7 @@ async function runOperatorTurn(input, deps) {
225245
225233
  getEnabledToolDefinitions(toolOpts),
225246
225234
  PLAN_MODE_ALLOWED_TOOL_NAMES
225247
225235
  ) : effectiveChatMode === "agents" ? getExecutableToolDefinitions(toolOpts) : effectiveChatMode === "ask" ? [] : getReadOnlyToolDefinitions(toolOpts);
225248
- const personaFilteredTools = enrichedInput.personaId === "quill" ? filterToolsForQuill(baseTurnToolDefinitions) : enrichedInput.personaId === "saffron" ? filterToolsForSaffron(baseTurnToolDefinitions, {
225249
- allowSuiteRelay: suiteRelayEnabled
225250
- }) : baseTurnToolDefinitions;
225251
- const suiteRelayFilteredTools = filterSuiteRelayTools(personaFilteredTools, {
225236
+ const suiteRelayFilteredTools = filterSuiteRelayTools(baseTurnToolDefinitions, {
225252
225237
  allowSuiteRelay: suiteRelayEnabled
225253
225238
  });
225254
225239
  const turnToolDefinitions = filterMainPersonaDeliveryTools(suiteRelayFilteredTools, {
@@ -225936,8 +225921,7 @@ var init_runOperatorTurn = __esm({
225936
225921
  init_folderIndexing();
225937
225922
  init_approvalResume();
225938
225923
  init_personaRegistry();
225939
- init_quillToolPolicy();
225940
- init_saffronToolPolicy();
225924
+ init_sharedToolPolicy();
225941
225925
  init_deliveryToolPolicy();
225942
225926
  init_voiceFilters();
225943
225927
  init_progressEventBridge();
@@ -283848,7 +283832,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
283848
283832
  text: `bash \xB7 ${truncateMiddle(commandText, 54)} \xB7 running`,
283849
283833
  tone: "muted",
283850
283834
  detailLines: [
283851
- { tone: "command", text: `$ ${commandText}` },
283835
+ { tone: "command", text: `$ ${commandText}`, language: "bash" },
283852
283836
  { tone: "meta", text: `cwd ${event.cwd || "."}` }
283853
283837
  ],
283854
283838
  expanded: false
@@ -283896,7 +283880,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
283896
283880
  richToolIds.current.add(itemId2);
283897
283881
  const commandText = renderCommandLine(event.command, event.args);
283898
283882
  const details = [
283899
- { tone: "command", text: `$ ${commandText}` },
283883
+ { tone: "command", text: `$ ${commandText}`, language: "bash" },
283900
283884
  { tone: "meta", text: `cwd ${event.cwd || "."}` },
283901
283885
  ...outputChunkToDetailLines(event.stdout, "stdout"),
283902
283886
  ...outputChunkToDetailLines(event.stderr, "stderr")
@@ -283929,9 +283913,10 @@ async function runInkInteractivePerchCli(writer, deps, options) {
283929
283913
  detailLines: [
283930
283914
  {
283931
283915
  tone: "command",
283932
- text: event.language === "shell" ? `$ ${event.command}` : `${event.language} cell`
283916
+ text: event.language === "shell" ? `$ ${event.command}` : `${event.language} cell`,
283917
+ language: event.language === "shell" ? "bash" : void 0
283933
283918
  },
283934
- ...event.language === "shell" ? [] : codePreviewDetailLines(event.command)
283919
+ ...event.language === "shell" ? [] : codePreviewDetailLines(event.command, cliLanguageForSandbox(event.language))
283935
283920
  ],
283936
283921
  expanded: false
283937
283922
  });
@@ -284124,14 +284109,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
284124
284109
  React11.createElement(
284125
284110
  Ink2.Box,
284126
284111
  { flexGrow: 1 },
284127
- React11.createElement(
284128
- Ink2.Text,
284129
- {
284130
- color: colorForInkDetailTone(line.tone),
284131
- dimColor: line.tone === "meta"
284132
- },
284133
- formatInkDetailLine(line)
284134
- )
284112
+ renderInkDetailContent(React11, Ink2, line)
284135
284113
  )
284136
284114
  );
284137
284115
  const renderTranscriptItem = (item, index) => {
@@ -284818,6 +284796,53 @@ function bodyColorForInkTone(tone) {
284818
284796
  return "#fff8f0";
284819
284797
  }
284820
284798
  }
284799
+ function renderInkDetailContent(React11, Ink2, line) {
284800
+ const prefix = inkDetailPrefix(line.tone);
284801
+ const baseColor = colorForInkDetailTone(line.tone);
284802
+ const language = normalizeCliDetailLanguage(line.language);
284803
+ const tokens = shouldSyntaxHighlightDetail(line) ? tokenizeCliDetailSyntax(language, line.text) : [{ text: line.text, tone: "plain" }];
284804
+ return React11.createElement(
284805
+ Ink2.Text,
284806
+ null,
284807
+ prefix ? React11.createElement(
284808
+ Ink2.Text,
284809
+ { color: baseColor, bold: line.tone === "add" || line.tone === "remove" },
284810
+ prefix
284811
+ ) : null,
284812
+ ...tokens.map(
284813
+ (token, index) => React11.createElement(
284814
+ Ink2.Text,
284815
+ {
284816
+ key: `${line.tone}-${index}-${token.tone}`,
284817
+ color: colorForCliSyntaxTone(token.tone, line.tone),
284818
+ dimColor: line.tone === "meta" || token.tone === "comment"
284819
+ },
284820
+ token.text
284821
+ )
284822
+ )
284823
+ );
284824
+ }
284825
+ function shouldSyntaxHighlightDetail(line) {
284826
+ if (line.tone === "meta" || line.tone === "hunk") return false;
284827
+ if (line.tone === "stdout" || line.tone === "stderr") return false;
284828
+ return Boolean(normalizeCliDetailLanguage(line.language)) || line.tone === "command";
284829
+ }
284830
+ function inkDetailPrefix(tone) {
284831
+ switch (tone) {
284832
+ case "add":
284833
+ return "+ ";
284834
+ case "remove":
284835
+ return "- ";
284836
+ case "stderr":
284837
+ return "! ";
284838
+ case "meta":
284839
+ return "# ";
284840
+ case "stdout":
284841
+ return " ";
284842
+ default:
284843
+ return "";
284844
+ }
284845
+ }
284821
284846
  function colorForInkDetailTone(tone) {
284822
284847
  switch (tone) {
284823
284848
  case "add":
@@ -284836,21 +284861,168 @@ function colorForInkDetailTone(tone) {
284836
284861
  return CLI_BRAND.cream;
284837
284862
  }
284838
284863
  }
284839
- function formatInkDetailLine(line) {
284840
- switch (line.tone) {
284841
- case "add":
284842
- return `+ ${line.text}`;
284843
- case "remove":
284844
- return `- ${line.text}`;
284845
- case "stderr":
284846
- return `! ${line.text}`;
284847
- case "meta":
284848
- return `# ${line.text}`;
284849
- case "stdout":
284850
- return ` ${line.text}`;
284851
- default:
284852
- return line.text;
284864
+ function colorForCliSyntaxTone(tone, lineTone) {
284865
+ if (lineTone === "remove") {
284866
+ switch (tone) {
284867
+ case "comment":
284868
+ return "#8e6a55";
284869
+ case "string":
284870
+ return "#c48656";
284871
+ case "keyword":
284872
+ case "flag":
284873
+ return CLI_BRAND.bronzeGlint;
284874
+ case "number":
284875
+ return "#c77952";
284876
+ case "operator":
284877
+ return CLI_BRAND.bronzeDeep;
284878
+ default:
284879
+ return CLI_BRAND.bronzeGlint;
284880
+ }
284881
+ }
284882
+ if (lineTone === "add") {
284883
+ switch (tone) {
284884
+ case "comment":
284885
+ return "#7f9586";
284886
+ case "string":
284887
+ return "#b7c989";
284888
+ case "keyword":
284889
+ case "flag":
284890
+ return CLI_BRAND.patinaActive;
284891
+ case "number":
284892
+ return "#8fd19c";
284893
+ case "operator":
284894
+ return "#7e9f87";
284895
+ default:
284896
+ return CLI_BRAND.cream;
284897
+ }
284898
+ }
284899
+ if (lineTone === "command") {
284900
+ switch (tone) {
284901
+ case "comment":
284902
+ return "#7a6f66";
284903
+ case "string":
284904
+ case "path":
284905
+ return "#e5bc75";
284906
+ case "keyword":
284907
+ return CLI_BRAND.bronzeGlint;
284908
+ case "flag":
284909
+ return CLI_BRAND.patinaActive;
284910
+ case "number":
284911
+ return "#8fd19c";
284912
+ case "operator":
284913
+ return CLI_BRAND.muted;
284914
+ default:
284915
+ return CLI_BRAND.cream;
284916
+ }
284853
284917
  }
284918
+ return colorForInkDetailTone(lineTone);
284919
+ }
284920
+ function tokenizeCliDetailSyntax(language, text) {
284921
+ const normalized = language ?? "text";
284922
+ const pattern = cliSyntaxPatternForLanguage(normalized);
284923
+ if (!pattern) return [{ text, tone: "plain" }];
284924
+ const tokens = [];
284925
+ let lastIndex = 0;
284926
+ pattern.lastIndex = 0;
284927
+ for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
284928
+ if (match.index > lastIndex) {
284929
+ tokens.push({ text: text.slice(lastIndex, match.index), tone: "plain" });
284930
+ }
284931
+ tokens.push({ text: match[0], tone: cliToneForSyntaxToken(normalized, match[0]) });
284932
+ lastIndex = match.index + match[0].length;
284933
+ }
284934
+ if (lastIndex < text.length) {
284935
+ tokens.push({ text: text.slice(lastIndex), tone: "plain" });
284936
+ }
284937
+ return tokens.length ? tokens : [{ text, tone: "plain" }];
284938
+ }
284939
+ function cliSyntaxPatternForLanguage(language) {
284940
+ if (["javascript", "typescript", "jsx", "tsx"].includes(language)) {
284941
+ return /(\/\/.*$|\/\*.*?\*\/|`(?:\\.|[^`])*`|'(?:\\.|[^'])*'|"(?:\\.|[^"])*"|\b(?:const|let|var|function|return|if|else|for|while|import|export|from|class|extends|new|await|async|try|catch|throw|type|interface|implements|switch|case|break|continue|null|undefined|true|false)\b|\b\d+(?:\.\d+)?\b|=>|===|!==|==|!=|\|\||&&|[{}()[\].,:;<>/+*=-])/g;
284942
+ }
284943
+ if (language === "python") {
284944
+ return /(#.*$|'''[\s\S]*?'''|"""[\s\S]*?"""|'(?:\\.|[^'])*'|"(?:\\.|[^"])*"|\b(?:def|class|return|if|elif|else|for|while|import|from|as|try|except|raise|with|yield|lambda|None|True|False|async|await|pass|break|continue|and|or|not)\b|\b\d+(?:\.\d+)?\b|[{}()[\].,:;<>/+*=-])/g;
284945
+ }
284946
+ if (language === "bash" || language === "shell") {
284947
+ return /(#.*$|'[^']*'|"(?:\\.|[^"])*"|\$(?:\w+|\{[^}]+\})|(?:^|\s)-{1,2}[A-Za-z0-9][\w-]*|\b(?:if|then|fi|for|do|done|case|esac|function|export|local|sudo|cd|echo|grep|sed|awk|find|cat|ls|mkdir|rm|mv|cp|npm|node|tsx|python|git|rg)\b|\b\d+\b|[|&;()<>])/g;
284948
+ }
284949
+ if (language === "json" || language === "yaml") {
284950
+ return /("(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|\b(?:true|false|null)\b|\b\d+(?:\.\d+)?\b|[:{}\[\],-])/g;
284951
+ }
284952
+ if (language === "css") {
284953
+ return /(\/\*.*?\*\/|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|#[0-9a-fA-F]{3,8}\b|\b\d+(?:\.\d+)?(?:px|rem|em|%)?\b|[{}:;(),])/g;
284954
+ }
284955
+ if (language === "html" || language === "xml") {
284956
+ return /(<!--.*?-->|<\/?[A-Za-z0-9:-]+|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\/?>)/g;
284957
+ }
284958
+ return null;
284959
+ }
284960
+ function cliToneForSyntaxToken(language, text) {
284961
+ if (text.startsWith("//") || text.startsWith("/*") || text.startsWith("#") || text.startsWith("<!--")) {
284962
+ return "comment";
284963
+ }
284964
+ if (text.startsWith("'") || text.startsWith('"') || text.startsWith("`")) {
284965
+ return language === "json" && text.endsWith(":") ? "keyword" : "string";
284966
+ }
284967
+ if (/^\s*-{1,2}[A-Za-z0-9]/.test(text)) return "flag";
284968
+ if (/^\$/.test(text)) return "keyword";
284969
+ if (/^\d/.test(text)) return "number";
284970
+ if (/^[{}()[\].,:;<>/+*=\-|&]+$/.test(text)) return "operator";
284971
+ if (/[\\/]/.test(text) && !/\s/.test(text)) return "path";
284972
+ return "keyword";
284973
+ }
284974
+ function normalizeCliDetailLanguage(language) {
284975
+ const value = language?.trim().toLowerCase();
284976
+ if (!value) return null;
284977
+ const map2 = {
284978
+ js: "javascript",
284979
+ mjs: "javascript",
284980
+ cjs: "javascript",
284981
+ jsx: "jsx",
284982
+ ts: "typescript",
284983
+ mts: "typescript",
284984
+ cts: "typescript",
284985
+ tsx: "tsx",
284986
+ py: "python",
284987
+ python3: "python",
284988
+ sh: "bash",
284989
+ shell: "bash",
284990
+ zsh: "bash",
284991
+ yml: "yaml",
284992
+ htm: "html"
284993
+ };
284994
+ return map2[value] ?? value;
284995
+ }
284996
+ function cliLanguageForSandbox(language) {
284997
+ if (language === "node") return "javascript";
284998
+ if (language === "shell") return "bash";
284999
+ return normalizeCliDetailLanguage(language) ?? "text";
285000
+ }
285001
+ function inferCliLanguageFromPath(filePath) {
285002
+ const extension2 = filePath.split(/[./\\]/).pop()?.toLowerCase();
285003
+ if (!extension2 || extension2 === filePath.toLowerCase()) return null;
285004
+ const map2 = {
285005
+ js: "javascript",
285006
+ mjs: "javascript",
285007
+ cjs: "javascript",
285008
+ jsx: "jsx",
285009
+ ts: "typescript",
285010
+ mts: "typescript",
285011
+ cts: "typescript",
285012
+ tsx: "tsx",
285013
+ py: "python",
285014
+ sh: "bash",
285015
+ zsh: "bash",
285016
+ json: "json",
285017
+ jsonc: "json",
285018
+ yml: "yaml",
285019
+ yaml: "yaml",
285020
+ css: "css",
285021
+ html: "html",
285022
+ htm: "html",
285023
+ xml: "xml"
285024
+ };
285025
+ return map2[extension2] ?? null;
284854
285026
  }
284855
285027
  function buildFileToolDisplay(toolName, input, phase, summary) {
284856
285028
  const normalizedName = toolName.toLowerCase();
@@ -284861,12 +285033,13 @@ function buildFileToolDisplay(toolName, input, phase, summary) {
284861
285033
  const filePath = stringValue8(input.path) ?? stringValue8(input.filePath) ?? summary?.filePath ?? "file";
284862
285034
  const short = shortFilePath(filePath);
284863
285035
  const status = phase === "running" ? "running" : summary?.changeKind ?? "done";
285036
+ const language = inferCliLanguageFromPath(filePath);
284864
285037
  if (isWrite) {
284865
285038
  const content = stringValue8(input.content) ?? "";
284866
285039
  const added = summary?.linesAdded ?? countTextLines(content);
284867
285040
  const detailLines = [
284868
285041
  { tone: "hunk", text: `@@ ${short}` },
284869
- ...textToDetailLines(content, "add")
285042
+ ...textToDetailLines(content, "add", language)
284870
285043
  ];
284871
285044
  if (detailLines.length === 1 && summary) {
284872
285045
  detailLines.push({ tone: "meta", text: describeChangeSummary(summary) });
@@ -284883,8 +285056,8 @@ function buildFileToolDisplay(toolName, input, phase, summary) {
284883
285056
  const added = summary?.linesAdded ?? countTextLines(newText);
284884
285057
  const detailLines = [
284885
285058
  { tone: "hunk", text: `@@ ${short}` },
284886
- ...textToDetailLines(oldText, "remove"),
284887
- ...textToDetailLines(newText, "add")
285059
+ ...textToDetailLines(oldText, "remove", language),
285060
+ ...textToDetailLines(newText, "add", language)
284888
285061
  ];
284889
285062
  if (detailLines.length === 1 && summary) {
284890
285063
  detailLines.push({ tone: "meta", text: describeChangeSummary(summary) });
@@ -284910,18 +285083,23 @@ function describeChangeSummary(summary) {
284910
285083
  const removed = summary.linesRemoved ?? 0;
284911
285084
  return `${kind} \xB7 +${added} -${removed}`;
284912
285085
  }
284913
- function textToDetailLines(text, tone) {
285086
+ function textToDetailLines(text, tone, language) {
284914
285087
  if (!text) return [];
284915
285088
  const lines = text.split(/\r?\n/);
284916
- const preview = lines.slice(0, 38).map((line) => ({ tone, text: line }));
285089
+ const normalizedLanguage = normalizeCliDetailLanguage(language);
285090
+ const preview = lines.slice(0, 38).map((line) => ({
285091
+ tone,
285092
+ text: line,
285093
+ ...normalizedLanguage ? { language: normalizedLanguage } : {}
285094
+ }));
284917
285095
  if (lines.length > preview.length) {
284918
285096
  preview.push({ tone: "meta", text: `${lines.length - preview.length} more line(s)` });
284919
285097
  }
284920
285098
  return preview;
284921
285099
  }
284922
- function codePreviewDetailLines(code) {
285100
+ function codePreviewDetailLines(code, language) {
284923
285101
  if (!code.trim()) return [];
284924
- return textToDetailLines(code, "command").slice(0, 18);
285102
+ return textToDetailLines(code, "command", language).slice(0, 18);
284925
285103
  }
284926
285104
  function outputChunkToDetailLines(text, tone) {
284927
285105
  if (!text) return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perchai-cli",
3
- "version": "2.4.20",
3
+ "version": "2.4.21",
4
4
  "description": "Perch AI command-line interface",
5
5
  "bin": {
6
6
  "perch": "bin/perch"