pi-freeflow 1.36.0 → 1.36.3

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.
@@ -53,7 +53,17 @@ export const PI_TOOL_NAMES = [
53
53
  "ls",
54
54
  ] as const;
55
55
 
56
- /** OMP host built-ins: BUILTIN_TOOL_NAMES in reference/oh-my-pi packages/coding-agent/src/tools/builtin-names.ts plus browser/computer */
56
+ /**
57
+ * OMP host built-ins: BUILTIN_TOOL_NAMES in
58
+ * reference/oh-my-pi/packages/coding-agent/src/tools/builtin-names.ts, plus
59
+ * browser/computer (real top-level tools in the same checkout) and `hub`.
60
+ *
61
+ * `find` here is OMP's semantic search (reference/oh-my-pi
62
+ * tools/jfind/index.ts:54), NOT Pi's file finder: they are different tools
63
+ * with different schemas, and both are declared together whenever the session
64
+ * enables it. That is why the translator renames `find` to `glob` only when the
65
+ * caller left the `glob` slot empty — see translateToolsForPath.
66
+ */
57
67
  export const OMP_TOOL_NAMES = [
58
68
  "read",
59
69
  "bash",
@@ -62,15 +72,20 @@ export const OMP_TOOL_NAMES = [
62
72
  "ast_edit",
63
73
  "ask",
64
74
  "debug",
75
+ "ida",
65
76
  "eval",
66
77
  "github",
67
78
  "glob",
68
79
  "grep",
80
+ "find",
69
81
  "lsp",
70
82
  "checkpoint",
71
83
  "rewind",
84
+ "context_notes",
85
+ "new_context",
72
86
  "security_scan",
73
87
  "task",
88
+ "wait",
74
89
  "hub",
75
90
  "todo",
76
91
  "web_search",
@@ -175,18 +190,58 @@ export const OMP_FINGERPRINT_DEFS: Record<Exclude<FingerprintToolName, "edit">,
175
190
  },
176
191
  };
177
192
 
193
+ /** Minimal valid schemas for injected compat tools to satisfy upstream inference gate. */
194
+ export const COMPAT_FINGERPRINT_SCHEMAS: Record<FingerprintToolName, Record<string, unknown>> = {
195
+ bash: {
196
+ type: "object",
197
+ properties: { command: { type: "string", description: "Shell command to run." } },
198
+ required: ["command"],
199
+ },
200
+ read: {
201
+ type: "object",
202
+ properties: { path: { type: "string", description: "File path to read." } },
203
+ required: ["path"],
204
+ },
205
+ glob: {
206
+ type: "object",
207
+ properties: { path: { type: "string", description: "Search path." } },
208
+ },
209
+ grep: {
210
+ type: "object",
211
+ properties: { pattern: { type: "string", description: "Search pattern." } },
212
+ required: ["pattern"],
213
+ },
214
+ edit: {
215
+ type: "object",
216
+ properties: { path: { type: "string", description: "File path to edit." } },
217
+ required: ["path"],
218
+ },
219
+ write: {
220
+ type: "object",
221
+ properties: { path: { type: "string", description: "File path to write." } },
222
+ required: ["path"],
223
+ },
224
+ };
225
+
178
226
  /** Caller tool names that only an OMP host sends (never Pi). */
179
227
  export const OMP_CALLER_MARKERS = ["ask", "task", "todo", "hub", "lsp"] as const;
180
228
 
181
229
  /**
182
230
  * True when a lowercased caller-tool name set looks like OMP: any of
183
- * ask/task/todo/hub/lsp present, or glob present without find (Pi sends find,
184
- * OMP sends glob). Case-insensitive; pass already-lowercased names.
231
+ * ask/task/todo/hub/lsp present, or `glob` present at all.
232
+ *
233
+ * `glob` alone is the discriminator because Pi has no such tool (its file
234
+ * finder is `find`, reference/pi core/tools/index.ts:95) while OMP declares
235
+ * both. The old test was "glob without find", which assumed `find` was
236
+ * Pi-exclusive; OMP ships a semantic `find` too (reference/oh-my-pi
237
+ * tools/jfind/index.ts:54), so an OMP read-only session carrying glob+find was
238
+ * classified as Pi and its fingerprint slots came back as non-executable
239
+ * placeholders. Case-insensitive; pass already-lowercased names.
185
240
  */
186
241
  export function isOmpLikeCaller(lowerNames: Iterable<string>): boolean {
187
242
  const set = lowerNames instanceof Set ? lowerNames : new Set(lowerNames);
188
243
  for (const m of OMP_CALLER_MARKERS) if (set.has(m)) return true;
189
- return set.has("glob") && !set.has("find");
244
+ return set.has("glob");
190
245
  }
191
246
 
192
247
  export interface CanonicalTool {
@@ -385,8 +440,14 @@ export function sanitizeUpstreamToolName(name: string, taken: ReadonlySet<string
385
440
  }
386
441
  /** Inspect the original caller tools (pre-translate) for the rewrite cases. */
387
442
  export function buildFindGlobRestore(callerTools: unknown[]): FindGlobRestore {
388
- let sawFind = false;
389
- let sawGlob = false;
443
+ // Same rule as translateToolsForPath: the find->glob rename fires only when
444
+ // the caller left the `glob` slot empty. OMP declares both real tools, so
445
+ // there the rename must not happen and `find` must keep its own name.
446
+ const callerDeclaresGlob = callerTools.some((tool) => {
447
+ const canon = canonicalizeTool(tool);
448
+ return canon !== null && canon.name.trim().toLowerCase() === "glob";
449
+ });
450
+ const nameFor = (name: string): string => (callerDeclaresGlob ? name.trim() : upstreamToolNameFor(name));
390
451
  const renamed: Record<string, string> = {};
391
452
  // Same first-seen-wins order and case rules as translateToolsForPath, so
392
453
  // the recorded map keys match the names that actually reach upstream.
@@ -395,25 +456,28 @@ export function buildFindGlobRestore(callerTools: unknown[]): FindGlobRestore {
395
456
  const canon = canonicalizeTool(tool);
396
457
  if (!canon) continue;
397
458
  const callerName = canon.name;
398
- if (isFindToolName(callerName)) sawFind = true;
399
- else if (callerName.trim().toLowerCase() === "glob") sawGlob = true;
459
+ const upstreamName = nameFor(callerName);
400
460
  // Mirrors translateToolsForPath: dedup on the lowercased
401
461
  // pre-sanitization name (a collapsed duplicate never reaches upstream),
402
462
  // then sanitize with the caller's casing intact.
403
- const dedupKey = upstreamToolNameFor(callerName).toLowerCase();
463
+ const dedupKey = upstreamName.toLowerCase();
404
464
  if (seen.has(dedupKey)) continue;
405
465
  seen.add(dedupKey);
406
- const sanitized = sanitizeUpstreamToolName(upstreamToolNameFor(callerName), seen);
466
+ const sanitized = sanitizeUpstreamToolName(upstreamName, seen);
407
467
  if (sanitized.toLowerCase() !== dedupKey) seen.add(sanitized.toLowerCase());
408
468
  // Only character-folding rewrites belong in the sanitized map. The
409
- // find->glob rename is a collision case with its own flag: when the
410
- // caller also sent `glob`, upstream `glob` really is the caller's glob,
411
- // so recording it here would misroute a genuine call.
469
+ // find->glob rename is a single-name fallback, so a name that came in
470
+ // as `find` must not be recorded here: upstream `glob` may be the
471
+ // caller's own glob.
412
472
  if (sanitized !== callerName && !isFindToolName(callerName)) {
413
473
  renamed[sanitized.toLowerCase()] = callerName;
414
474
  }
415
475
  }
416
- const restored: FindGlobRestore = { renamedFindToGlob: sawFind && !sawGlob, renamed };
476
+ const sawFind = callerTools.some((tool) => {
477
+ const canon = canonicalizeTool(tool);
478
+ return canon !== null && isFindToolName(canon.name);
479
+ });
480
+ const restored: FindGlobRestore = { renamedFindToGlob: sawFind && !callerDeclaresGlob, renamed };
417
481
  return restored;
418
482
  }
419
483
  /**
@@ -489,22 +553,46 @@ function carryExtraFields(
489
553
  }
490
554
  }
491
555
  /**
492
- * Reshape a caller tool array for the target path. Tools already in the
493
- * target shape pass through verbatim (identical reference, every extra field
494
- * intact) except Pi `find`, which is renamed to `glob` even when the shape
495
- * already matches (new object, params/description verbatim). Function tools
496
- * in another shape are converted, carrying `description`/`parameters`/`strict`
497
- * verbatim plus every other top-level caller field except the
498
- * wire-shape-forbidden keys (`type`/`function` wrapper, `name`, `description`,
499
- * `parameters`/`input_schema`), which are remapped per target shape. Never
500
- * injects `additionalProperties`. Every other entry passes through verbatim.
501
- * Duplicates collapse first-seen-wins on the lowercased upstream name, so
502
- * `Bash` never duplicates `bash` and `find` collapses with `glob`.
503
- * Re-translating translated output adds zero tools. Never returns null entries.
556
+ * Reshape a caller tool array for the target path. Tools already in the target
557
+ * shape pass through verbatim (identical reference, every extra field intact).
558
+ * Function tools in another shape are converted, carrying
559
+ * `description`/`parameters`/`strict` verbatim plus every other top-level
560
+ * caller field except the wire-shape-forbidden keys (`type`/`function`
561
+ * wrapper, `name`, `description`, `parameters`/`input_schema`), which are
562
+ * remapped per target shape. Never injects `additionalProperties`. Every other
563
+ * entry passes through verbatim. Duplicates collapse first-seen-wins on the
564
+ * lowercased upstream name, so `Bash` never duplicates `bash`. Re-translating
565
+ * translated output adds zero tools. Never returns null entries.
504
566
  * `tool_choice` is never imposed here; use the retarget helpers to carry it.
567
+ *
568
+ * Names ride the caller's own unless `forFingerprint` is set — that flag exists
569
+ * for the OpenCode free tier alone, whose gate expects the sextet name `glob`
570
+ * and therefore needs Pi's `find` renamed into it. On every other lane the
571
+ * rename is pure loss: the upstream would have accepted `find`, and the host
572
+ * gets `find` back and dispatches it natively.
505
573
  */
506
- export function translateToolsForPath(tools: unknown[], pathname: string): Record<string, unknown>[] {
574
+ export function translateToolsForPath(tools: unknown[], pathname: string, opts?: { forFingerprint?: boolean }): Record<string, unknown>[] {
507
575
  const api = apiForPathname(pathname);
576
+ // Only the OpenCode free tier needs a tool renamed: its gate looks for a
577
+ // sextet whose `glob` slot Pi's `find` can fill. Every other upstream accepts
578
+ // the caller's own names, so on those lanes the name rides verbatim in BOTH
579
+ // directions and the host dispatches it natively — nothing to translate, and
580
+ // nothing to restore.
581
+ //
582
+ // Even on the fingerprint lane the rename is skipped when the caller declared
583
+ // `glob` itself: OMP declares both `glob` (file patterns) and `find`
584
+ // (semantic search, reference/oh-my-pi tools/jfind/index.ts:54), and renaming
585
+ // there would collapse two real tools into one under the other's schema.
586
+ let callerDeclaresGlob = false;
587
+ for (const tool of tools) {
588
+ const canon = canonicalizeTool(tool);
589
+ if (canon && canon.name.trim().toLowerCase() === "glob") {
590
+ callerDeclaresGlob = true;
591
+ break;
592
+ }
593
+ }
594
+ const renameFind = opts?.forFingerprint === true && !callerDeclaresGlob;
595
+ const upstreamNameFor = (name: string): string => (renameFind ? upstreamToolNameFor(name) : name.trim());
508
596
  const seen = new Set<string>();
509
597
  const out: Record<string, unknown>[] = [];
510
598
  for (const tool of tools) {
@@ -517,15 +605,16 @@ export function translateToolsForPath(tools: unknown[], pathname: string): Recor
517
605
  continue;
518
606
  }
519
607
  // Duplicate collapse stays first-seen-wins on the lowercased upstream name
520
- // (`Bash` never duplicates `bash`, `find` collapses with `glob`) and runs on
521
- // the pre-sanitization name: the emitted name keeps the caller's casing, and
522
- // the sanitizer only ever folds characters the upstream shape forbids.
523
- const dedupKey = upstreamToolNameFor(canon.name).toLowerCase();
608
+ // (`Bash` never duplicates `bash`, a Pi `find` still collapses with `glob`)
609
+ // and runs on the pre-sanitization name: the emitted name keeps the caller's
610
+ // casing, and the sanitizer only ever folds characters the upstream shape
611
+ // forbids.
612
+ const dedupKey = upstreamNameFor(canon.name).toLowerCase();
524
613
  if (seen.has(dedupKey)) continue;
525
614
  seen.add(dedupKey);
526
615
  // The find->glob rename and the name sanitization share one upstream name,
527
616
  // so a caller tool needing either is emitted under a single new name.
528
- const upstreamName = sanitizeUpstreamToolName(upstreamToolNameFor(canon.name), seen);
617
+ const upstreamName = sanitizeUpstreamToolName(upstreamNameFor(canon.name), seen);
529
618
  if (upstreamName.toLowerCase() !== dedupKey) seen.add(upstreamName.toLowerCase());
530
619
  const needsRename = upstreamName !== canon.name;
531
620
  if (!needsRename && alreadyTargetShape(rec, api)) {
@@ -535,60 +624,17 @@ export function translateToolsForPath(tools: unknown[], pathname: string): Recor
535
624
  const renamed: CanonicalTool = needsRename
536
625
  ? { ...canon, name: upstreamName }
537
626
  : canon;
538
- const converted =
539
- api === "responses"
540
- ? toResponsesTool(renamed)
541
- : api === "messages"
542
- ? toAnthropicTool(renamed)
543
- : toChatTool(renamed);
544
- carryExtraFields(rec, converted);
545
- out.push(converted);
627
+ const converted =
628
+ api === "responses"
629
+ ? toResponsesTool(renamed)
630
+ : api === "messages"
631
+ ? toAnthropicTool(renamed)
632
+ : toChatTool(renamed);
633
+ carryExtraFields(rec, converted);
634
+ out.push(converted);
546
635
  }
547
636
  return out;
548
637
  }
549
- /**
550
- * Inject the missing fingerprint tools into an already-translated tool array,
551
- * using the target path's shape. Idempotent and case-insensitive: `Bash`
552
- * satisfies `bash` and is never duplicated.
553
- * OMP-like callers (explicit `ompLike`, else auto-detected from the array via
554
- * {@link isOmpLikeCaller}) get REAL minimal executable definitions from
555
- * {@link OMP_FINGERPRINT_DEFS}; everyone else (Pi) keeps the empty COMPAT
556
- * placeholders. `edit` is always the COMPAT placeholder, even for OMP-like
557
- * callers — no static edit def executes on either host, so it must stay
558
- * cloaked downstream. Re-injecting injected output adds zero tools.
559
- */
560
- export function injectFingerprintTools(
561
- tools: Record<string, unknown>[],
562
- pathname: string,
563
- ompLike?: boolean,
564
- ): Record<string, unknown>[] {
565
- const present = new Set<string>();
566
- for (const tool of tools) {
567
- const canon = canonicalizeTool(tool);
568
- if (canon) present.add(canon.name.trim().toLowerCase());
569
- }
570
- const omp = ompLike ?? isOmpLikeCaller(present);
571
- const api = apiForPathname(pathname);
572
- for (const name of OPENCODE_FINGERPRINT_TOOLS) {
573
- if (present.has(name.toLowerCase())) continue;
574
- const def = omp && name !== "edit" ? OMP_FINGERPRINT_DEFS[name] : null;
575
- const canon: CanonicalTool = {
576
- name,
577
- description: def ? def.description : COMPAT_TOOL_DESCRIPTION,
578
- parameters: def ? { ...(def.parameters as Record<string, unknown>) } : { type: "object", properties: {} },
579
- };
580
- tools.push(
581
- api === "responses"
582
- ? toResponsesTool(canon)
583
- : api === "messages"
584
- ? toAnthropicTool(canon)
585
- : toChatTool(canon),
586
- );
587
- present.add(name.toLowerCase());
588
- }
589
- return tools;
590
- }
591
-
592
638
  /**
593
639
  * Responses <-> Chat body translation. Cline serves chat completions only, so
594
640
  * the responses body is reshaped to chat before the Cline branch sends it,
@@ -675,7 +721,10 @@ export function chatBodyFromResponsesBody(body: Record<string, unknown>): Record
675
721
  out.messages = messages;
676
722
  if (Array.isArray(body.tools)) out.tools = translateToolsForPath(body.tools, "/v1/chat/completions");
677
723
  if (body.tool_choice !== undefined) out.tool_choice = body.tool_choice;
678
- for (const key of ["temperature", "top_p", "max_tokens", "max_completion_tokens", "stop", "presence_penalty", "frequency_penalty", "seed", "user", "parallel_tool_calls", "response_format"] as const) {
724
+ // `max_output_tokens` is the Responses-native cap and is what a host actually
725
+ // sets on this path; without it here the cap silently vanished on the
726
+ // chat-only lanes and the model answered at its own default length.
727
+ for (const key of ["temperature", "top_p", "max_tokens", "max_completion_tokens", "max_output_tokens", "stop", "presence_penalty", "frequency_penalty", "seed", "user", "parallel_tool_calls", "response_format"] as const) {
679
728
  if (body[key] !== undefined) out[key] = body[key];
680
729
  }
681
730
  return out;
@@ -741,8 +790,51 @@ export function chatResponsesJsonFromChatCompletion(chat: Record<string, unknown
741
790
  */
742
791
  export function chatResponsesSseFromChatCompletion(resp: Record<string, unknown>): string {
743
792
  const head = { type: "response.created", response: { id: resp.id, object: "response", model: resp.model } };
793
+ let out = `event: response.created\ndata: ${JSON.stringify(head)}\n\n`;
794
+ const output = resp.output;
795
+ if (Array.isArray(output)) {
796
+ for (let idx = 0; idx < output.length; idx++) {
797
+ const item = output[idx] as Record<string, unknown>;
798
+ if (item && item.type === "message" && Array.isArray(item.content)) {
799
+ for (const part of item.content) {
800
+ const p = part as Record<string, unknown>;
801
+ if (p && p.type === "output_text" && typeof p.text === "string" && p.text.length > 0) {
802
+ out += `event: response.output_text.delta\ndata: ${JSON.stringify({
803
+ type: "response.output_text.delta",
804
+ output_index: idx,
805
+ delta: p.text,
806
+ })}\n\n`;
807
+ }
808
+ }
809
+ } else if (item && item.type === "function_call") {
810
+ out += `event: response.output_item.added\ndata: ${JSON.stringify({
811
+ type: "response.output_item.added",
812
+ output_index: idx,
813
+ item: {
814
+ type: "function_call",
815
+ id: item.id,
816
+ call_id: item.call_id,
817
+ name: item.name,
818
+ arguments: "",
819
+ },
820
+ })}\n\n`;
821
+ if (typeof item.arguments === "string" && item.arguments.length > 0) {
822
+ out += `event: response.function_call_arguments.delta\ndata: ${JSON.stringify({
823
+ type: "response.function_call_arguments.delta",
824
+ output_index: idx,
825
+ delta: item.arguments,
826
+ })}\n\n`;
827
+ }
828
+ out += `event: response.output_item.done\ndata: ${JSON.stringify({
829
+ type: "response.output_item.done",
830
+ output_index: idx,
831
+ item,
832
+ })}\n\n`;
833
+ }
834
+ }
835
+ }
744
836
  const done = { type: "response.completed", response: resp };
745
- return `event: response.created\ndata: ${JSON.stringify(head)}\n\n` +
746
- `event: response.completed\ndata: ${JSON.stringify(done)}\n\n` +
747
- `data: [DONE]\n\n`;
837
+ out += `event: response.completed\ndata: ${JSON.stringify(done)}\n\n`;
838
+ out += `data: [DONE]\n\n`;
839
+ return out;
748
840
  }