pi2dsh 0.15.2 → 0.16.1

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 (64) hide show
  1. package/README.md +41 -60
  2. package/README.zh.md +34 -54
  3. package/dist/{analyzer-C5FQXB7R.mjs → analyzer-B-b4C_5O.mjs} +2 -2
  4. package/dist/{analyzer-C5FQXB7R.mjs.map → analyzer-B-b4C_5O.mjs.map} +1 -1
  5. package/dist/cli.mjs +2 -3
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/compat/pi-ai.d.mts +1 -1
  8. package/dist/compat/pi-ai.mjs +1 -2
  9. package/dist/compat/pi-coding-agent.mjs +1 -1
  10. package/dist/credentials-oauth.d.mts +8 -2
  11. package/dist/credentials-oauth.d.mts.map +1 -1
  12. package/dist/credentials-oauth.mjs +134 -1
  13. package/dist/credentials-oauth.mjs.map +1 -0
  14. package/dist/{google-generative-ai-2gCFYp-t.mjs → google-generative-ai-Dpb_X-Kt.mjs} +2 -2
  15. package/dist/{google-generative-ai-2gCFYp-t.mjs.map → google-generative-ai-Dpb_X-Kt.mjs.map} +1 -1
  16. package/dist/{google-shared-75IdVyrx.mjs → google-shared-CL1Za-ev.mjs} +12 -12
  17. package/dist/{google-shared-75IdVyrx.mjs.map → google-shared-CL1Za-ev.mjs.map} +1 -1
  18. package/dist/{google-vertex-Bhyo4z2i.mjs → google-vertex-BXRKa49Y.mjs} +2 -2
  19. package/dist/{google-vertex-Bhyo4z2i.mjs.map → google-vertex-BXRKa49Y.mjs.map} +1 -1
  20. package/dist/host.mjs +1 -1
  21. package/dist/index.d.mts.map +1 -1
  22. package/dist/index.mjs +3 -4
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/{compatibility-DVMqXJux.mjs → mcp-config-BNL_uhE8.mjs} +181 -4
  25. package/dist/mcp-config-BNL_uhE8.mjs.map +1 -0
  26. package/dist/{mistral-conversations-DJyeTo8C.mjs → mistral-conversations-D-Kf38nj.mjs} +5491 -407
  27. package/dist/mistral-conversations-D-Kf38nj.mjs.map +1 -0
  28. package/dist/{multipart-parser-SRcGkVuX.mjs → multipart-parser-CBAi6PxA.mjs} +2 -2
  29. package/dist/{multipart-parser-SRcGkVuX.mjs.map → multipart-parser-CBAi6PxA.mjs.map} +1 -1
  30. package/dist/{credentials-oauth-Didpkkjx.mjs → oauth-bridge-C_MAPMGA.mjs} +11 -88
  31. package/dist/oauth-bridge-C_MAPMGA.mjs.map +1 -0
  32. package/dist/oauth-bridge-hOnT3acY.d.mts +34 -0
  33. package/dist/oauth-bridge-hOnT3acY.d.mts.map +1 -0
  34. package/dist/{otel-CBhmS-NR.mjs → otel-EL651-JR.mjs} +2 -2
  35. package/dist/{otel-CBhmS-NR.mjs.map → otel-EL651-JR.mjs.map} +1 -1
  36. package/dist/{pi-ai-DpqiLdPA.mjs → pi-ai-DQPHUR9S.mjs} +252 -7
  37. package/dist/pi-ai-DQPHUR9S.mjs.map +1 -0
  38. package/dist/{pi-ai-ZU5MelXy.d.mts → pi-ai-hGbYES_v.d.mts} +2 -2
  39. package/dist/{pi-ai-ZU5MelXy.d.mts.map → pi-ai-hGbYES_v.d.mts.map} +1 -1
  40. package/dist/pi-coding-agent-69yY0ke-.d.mts.map +1 -1
  41. package/dist/{pi-coding-agent-DK8hbJP5.mjs → pi-coding-agent-DXJ_Jzo0.mjs} +0 -0
  42. package/dist/{pi-coding-agent-DK8hbJP5.mjs.map → pi-coding-agent-DXJ_Jzo0.mjs.map} +1 -1
  43. package/dist/{runtime-BlbKWw7W.mjs → runtime-Bjc6ijZg.mjs} +586 -82
  44. package/dist/runtime-Bjc6ijZg.mjs.map +1 -0
  45. package/dist/runtime.d.mts +95 -47
  46. package/dist/runtime.d.mts.map +1 -1
  47. package/dist/runtime.mjs +1 -1
  48. package/dist/{from-BCscBQ9P.mjs → src-85HOaW5W.mjs} +1185 -10
  49. package/dist/src-85HOaW5W.mjs.map +1 -0
  50. package/package.json +28 -21
  51. package/dist/compatibility-DVMqXJux.mjs.map +0 -1
  52. package/dist/completionchunk-Dhk56lZl.mjs +0 -5212
  53. package/dist/completionchunk-Dhk56lZl.mjs.map +0 -1
  54. package/dist/credentials-oauth-Didpkkjx.mjs.map +0 -1
  55. package/dist/from-BCscBQ9P.mjs.map +0 -1
  56. package/dist/mcp-config-jmzbgdHa.mjs +0 -181
  57. package/dist/mcp-config-jmzbgdHa.mjs.map +0 -1
  58. package/dist/mistral-conversations-DJyeTo8C.mjs.map +0 -1
  59. package/dist/openai-responses-BRdGpBX6.mjs +0 -251
  60. package/dist/openai-responses-BRdGpBX6.mjs.map +0 -1
  61. package/dist/pi-ai-DpqiLdPA.mjs.map +0 -1
  62. package/dist/runtime-BlbKWw7W.mjs.map +0 -1
  63. package/dist/src-DlTagfW5.mjs +0 -1186
  64. package/dist/src-DlTagfW5.mjs.map +0 -1
@@ -1,13 +1,13 @@
1
1
 
2
- import { En as PiCapabilityError, F as generateBranchSummary, Tn as CapabilityLedger, b as Theme, d as ExtensionRunner, w as __runWithSubagentSessionFactory } from "./pi-coding-agent-DK8hbJP5.mjs";
2
+ import { En as PiCapabilityError, F as generateBranchSummary, Tn as CapabilityLedger, b as Theme, d as ExtensionRunner, w as __runWithSubagentSessionFactory } from "./pi-coding-agent-DXJ_Jzo0.mjs";
3
3
  import { t as getAgentDir } from "./pi-config-shim-CZ1wFzqM.mjs";
4
- import { $ as isContextOverflow, Q as AssistantMessageEventStream, S as getSupportedThinkingLevels, c as builtinProviders, o as __runWithPiAiRuntime, r as __createPiAiRuntimeRegistry } from "./pi-ai-DpqiLdPA.mjs";
4
+ import { $ as AssistantMessageEventStream, S as getSupportedThinkingLevels, c as builtinProviders, et as isContextOverflow, o as __runWithPiAiRuntime, r as __createPiAiRuntimeRegistry } from "./pi-ai-DQPHUR9S.mjs";
5
5
  import { W as getKeybindings, _t as stripTerminalSequences } from "./pi-tui-DruSeKmd.mjs";
6
- import { a as FileCredentialStore, c as providerSupportsOAuth, i as oauthCredentialRef, l as resolvePiProviderAuth, o as joinSignals, s as loginPiProvider, u as storedOAuthCredential } from "./credentials-oauth-Didpkkjx.mjs";
6
+ import { c as resolvePiProviderAuth, i as oauthCredentialRef, l as storedOAuthCredential, n as joinSignals, o as providerSupportsOAuth, r as loginPiProvider, t as FileCredentialStore } from "./oauth-bridge-C_MAPMGA.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
9
9
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
10
- import { join } from "node:path";
10
+ import { basename, dirname, join, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { tmpdir } from "node:os";
13
13
  import { EventEmitter } from "node:events";
@@ -69,6 +69,37 @@ var PiSessionBridge = class {
69
69
  const safe = sessionId.replace(/[^a-zA-Z0-9._-]+/gu, "_");
70
70
  return join(sidecarDir(), `${safe}.jsonl`);
71
71
  }
72
+ /**
73
+ * The Pi-visible archive file for one DSH session — the established
74
+ * `<id>.jsonl` convention (getSessionFile/switchSession use the same one).
75
+ * Guaranteed to EXIST on return: Pi consumers treat the session file as the
76
+ * durable identity a conversation can be reopened by (pi-subagents guards
77
+ * its tombstone resurrect with existsSync, and Pi's SessionManager.open
78
+ * tolerates an empty file), so a merely virtual path would read as "the
79
+ * conversation is gone".
80
+ */
81
+ archiveFileFor(sessionId) {
82
+ const path = this.sidecarPath(sessionId);
83
+ if (!existsSync(path)) {
84
+ mkdirSync(sidecarDir(), { recursive: true });
85
+ appendFileSync(path, "");
86
+ }
87
+ return path;
88
+ }
89
+ /**
90
+ * The DSH session id an archive-file path names, or undefined for any path
91
+ * this bridge did not mint (a genuine Pi session file, an in-memory
92
+ * manager's undefined). The reverse of {@link archiveFileFor} — ids that
93
+ * survive its sanitization round-trip exactly, which every id this bridge
94
+ * mints does.
95
+ */
96
+ sessionIdOfArchiveFile(path) {
97
+ if (typeof path !== "string" || path.length === 0) return void 0;
98
+ const resolved = resolve(path);
99
+ if (resolve(dirname(resolved)) !== resolve(sidecarDir())) return void 0;
100
+ const base = basename(resolved);
101
+ return base.endsWith(".jsonl") ? base.slice(0, -6) : void 0;
102
+ }
72
103
  load(sessionId) {
73
104
  if (this.loaded.has(sessionId)) return;
74
105
  this.loaded.add(sessionId);
@@ -307,7 +338,7 @@ var PiSessionBridge = class {
307
338
  getCwd: () => cwd,
308
339
  getSessionDir: () => sidecarDir(),
309
340
  getSessionId: () => session.id,
310
- getSessionFile: () => this.sidecarPath(session.id),
341
+ getSessionFile: () => this.archiveFileFor(session.id),
311
342
  getLeafId: () => leafOf()?.id ?? null,
312
343
  getLeafEntry: () => leafOf(),
313
344
  getEntry: (id) => entriesOf().find((entry) => entry.id === id),
@@ -359,6 +390,26 @@ var PiSessionBridge = class {
359
390
  //#region src/subagent-bridge.ts
360
391
  let subagentSerial = 0;
361
392
  /**
393
+ * Pi built-in tool name → the DSH native tool that serves it. The two
394
+ * vocabularies coincide (read/bash/edit/write/grep); Pi's find and ls are
395
+ * both served by the host's glob.
396
+ */
397
+ function nativeToolNameOf(name) {
398
+ return name === "find" || name === "ls" ? "glob" : name;
399
+ }
400
+ /**
401
+ * The tool schemas a CHILD agent actually resolves — its scoped view when the
402
+ * service answers for the agent being built, else the global layer. On
403
+ * roster-owned surfaces the preset tools live above the global layer, so the
404
+ * scoped read is the one that sees them.
405
+ */
406
+ function childSchemas(toolsService, childCtx) {
407
+ try {
408
+ if (childCtx.agent !== void 0) return toolsService.schemas(childCtx.agent);
409
+ } catch {}
410
+ return toolsService.schemas();
411
+ }
412
+ /**
362
413
  * Cross-scope subscription base. Session events dispatch under the OWNING
363
414
  * session's scope; a package instance mounted per Agent registers listeners
364
415
  * inside its own Agent's scope and would never see a child session's events.
@@ -385,6 +436,12 @@ function piMessageText(message) {
385
436
  var PiBridgedAgentSession = class {
386
437
  agent = {};
387
438
  messages = [];
439
+ /**
440
+ * Pi's AgentSession.sessionManager surface, projected over the child's DSH
441
+ * session. getSessionFile() names the durable archive — what pi-subagents
442
+ * stores as the conversation's reopenable identity (tombstone resurrect).
443
+ */
444
+ sessionManager;
388
445
  #seed = [];
389
446
  #carried = /* @__PURE__ */ new WeakSet();
390
447
  #host;
@@ -398,8 +455,11 @@ var PiBridgedAgentSession = class {
398
455
  #model;
399
456
  #streaming = false;
400
457
  #pendingToolCalls = /* @__PURE__ */ new Set();
458
+ /** callId → tool name, so tool_execution_end can name the tool its result belongs to. */
459
+ #toolCallNames = /* @__PURE__ */ new Map();
401
460
  #sessionName = "";
402
461
  #turns = 0;
462
+ #restrictionDispose;
403
463
  /** Keeps message projections (which await attachment reads) in log order. */
404
464
  #messageProjection = Promise.resolve();
405
465
  #aborted = false;
@@ -407,6 +467,7 @@ var PiBridgedAgentSession = class {
407
467
  this.#host = host;
408
468
  this.#handle = handle;
409
469
  this.#session = handle.agent.session ?? {};
470
+ this.sessionManager = host.sessionManagerFor?.(this.#session);
410
471
  this.#tools = tools;
411
472
  this.#activeToolNames = tools.map((tool) => tool?.name).filter((name) => typeof name === "string");
412
473
  const cordis = unscopedEventContext(host.cordis);
@@ -493,6 +554,10 @@ var PiBridgedAgentSession = class {
493
554
  handler(piEvent);
494
555
  } catch {}
495
556
  };
557
+ if (this.#aborted && (type === "turn/start" || type === "step/start" || type === "request/header")) {
558
+ this.#cancelChild();
559
+ return;
560
+ }
496
561
  if (type === "turn/start") {
497
562
  this.#streaming = true;
498
563
  emit({
@@ -509,11 +574,32 @@ var PiBridgedAgentSession = class {
509
574
  }
510
575
  if (type === "tools/result" || type === "tool/result") {
511
576
  const data = event.data;
512
- this.#pendingToolCalls.delete(String(data.callId ?? ""));
577
+ const message = data.message;
578
+ const blocks = Array.isArray(message?.content) ? message.content : [];
579
+ for (const block of blocks) {
580
+ const record = block;
581
+ if (record?.type !== "tool-result") continue;
582
+ const callId = String(record.toolCallId ?? (message?.source)?.callId ?? data.callId ?? "");
583
+ this.#pendingToolCalls.delete(callId);
584
+ const isError = record.isError === true;
585
+ emit({
586
+ type: "tool_execution_end",
587
+ toolCallId: callId,
588
+ toolName: this.#toolCallNames.get(callId) ?? "",
589
+ result: {
590
+ content: record.content ?? [],
591
+ isError
592
+ },
593
+ isError
594
+ });
595
+ this.#toolCallNames.delete(callId);
596
+ }
597
+ if (blocks.length === 0) this.#pendingToolCalls.delete(String(data.callId ?? ""));
513
598
  }
514
599
  if (type === "tool/call") {
515
600
  const data = event.data;
516
601
  this.#pendingToolCalls.add(String(data.callId ?? ""));
602
+ this.#toolCallNames.set(String(data.callId ?? ""), String(data.name ?? ""));
517
603
  let args = {};
518
604
  try {
519
605
  args = JSON.parse(String(data.arguments ?? "{}"));
@@ -582,6 +668,7 @@ var PiBridgedAgentSession = class {
582
668
  return () => this.#subscribers.delete(handler);
583
669
  }
584
670
  async prompt(text) {
671
+ this.#aborted = false;
585
672
  if (this.#seed.length > 0) {
586
673
  const seeded = this.#seed;
587
674
  this.#seed = [];
@@ -645,13 +732,28 @@ var PiBridgedAgentSession = class {
645
732
  }
646
733
  abort() {
647
734
  this.#aborted = true;
735
+ this.#cancelChild();
736
+ }
737
+ /** One official Agent.cancel, contained but LOUD: a swallowed cancel is a
738
+ * child that keeps running after its parent was interrupted. */
739
+ #cancelChild() {
740
+ const cancel = this.#handle.agent.cancel;
741
+ if (cancel === void 0) {
742
+ const message = "[pi2dsh] subagent abort(): the child agent handle has no cancel — the child cannot be stopped";
743
+ console.warn(message);
744
+ this.#host.cordis.logger?.warn?.(message);
745
+ return;
746
+ }
648
747
  try {
649
- const cancel = this.#handle.agent.cancel;
650
- cancel?.({
748
+ cancel.call(this.#handle.agent, {
651
749
  kind: "hook",
652
750
  reason: "pi2dsh subagent abort()"
653
751
  });
654
- } catch {}
752
+ } catch (error) {
753
+ const message = `[pi2dsh] subagent abort(): Agent.cancel failed (${error instanceof Error ? error.message : String(error)}) — already disposed, or the child kept running`;
754
+ console.warn(message);
755
+ this.#host.cordis.logger?.warn?.(message);
756
+ }
655
757
  }
656
758
  setSessionName(name) {
657
759
  this.#sessionName = String(name);
@@ -666,14 +768,44 @@ var PiBridgedAgentSession = class {
666
768
  aborted: this.#aborted
667
769
  };
668
770
  }
771
+ /** The child scope's REAL tool schemas (native + scoped custom), in Pi tool shape. */
772
+ #scopeTools() {
773
+ const agentCtx = this.#handle.agent.ctx;
774
+ const toolsService = typeof agentCtx?.get === "function" ? agentCtx.get("tools") : void 0;
775
+ try {
776
+ return [...toolsService?.schemas?.(this.#handle.agent) ?? []];
777
+ } catch {
778
+ return [];
779
+ }
780
+ }
669
781
  getAllTools() {
670
- return [...this.#tools];
782
+ const custom = new Map(this.#tools.filter((tool) => typeof tool?.name === "string").map((tool) => [String(tool.name), tool]));
783
+ return this.#scopeTools().map((schema) => custom.get(schema.name) ?? schema);
671
784
  }
672
785
  getActiveToolNames() {
673
- return [...this.#activeToolNames];
786
+ const fromScope = this.#scopeTools().map((schema) => schema.name);
787
+ return fromScope.length > 0 ? fromScope : [...this.#activeToolNames];
788
+ }
789
+ /** Hand over the creation-time restriction so a later setActiveToolsByName
790
+ * retires it instead of intersecting with it. */
791
+ adoptRestriction(dispose) {
792
+ this.#restrictionDispose = dispose;
674
793
  }
675
794
  setActiveToolsByName(names) {
676
795
  this.#activeToolNames = names.filter((name) => typeof name === "string");
796
+ const agentCtx = this.#handle.agent.ctx;
797
+ const toolsService = typeof agentCtx?.get === "function" ? agentCtx.get("tools") : void 0;
798
+ if (toolsService === void 0) return;
799
+ try {
800
+ this.#restrictionDispose?.();
801
+ let known;
802
+ try {
803
+ known = new Set(toolsService.schemas(this.#handle.agent).map((schema) => schema.name));
804
+ } catch {
805
+ known = new Set(toolsService.schemas().map((schema) => schema.name));
806
+ }
807
+ this.#restrictionDispose = toolsService.restrict({ allow: this.#activeToolNames.map(nativeToolNameOf).filter((name) => known.has(name)) });
808
+ } catch {}
677
809
  }
678
810
  async bindExtensions(_bindings = {}) {}
679
811
  async dispose() {
@@ -708,45 +840,111 @@ function childLabel(requested, packageName) {
708
840
  async function createBridgedAgentSession(host, options) {
709
841
  const agents = host.cordis.get("agents");
710
842
  if (agents?.create === void 0) throw new Error("pi2dsh: createAgentSession() needs the DSH agent registry in the host composition");
843
+ const providedManager = options.sessionManager;
844
+ const archiveFile = typeof providedManager?.getSessionFile === "function" ? providedManager.getSessionFile() : void 0;
845
+ const resumeSessionId = host.resumeSessionIdFor?.(archiveFile);
711
846
  subagentSerial += 1;
712
847
  const sessionId = `pi2dsh-sub-${Date.now().toString(36)}-${subagentSerial}`;
713
848
  let handle;
849
+ let initialRestrictionDispose;
714
850
  try {
715
851
  const requestedModel = options.model;
716
852
  const loader = options.resourceLoader;
717
853
  const overrideText = typeof options.systemPrompt === "string" && options.systemPrompt.length > 0 ? options.systemPrompt : typeof loader?.getSystemPrompt === "function" ? loader.getSystemPrompt() : void 0;
718
854
  const appendTexts = (typeof loader?.getAppendSystemPrompt === "function" ? loader.getAppendSystemPrompt() : []).filter((text) => typeof text === "string" && text.length > 0);
719
855
  const systemPrompt = overrideText !== void 0 && overrideText.length > 0 ? [overrideText, ...appendTexts].join("\n\n") : void 0;
720
- handle = await agents.create({
856
+ const customTools = Array.isArray(options.customTools) ? [...options.customTools] : [];
857
+ const requestedNames = Array.isArray(options.tools) ? options.tools.filter((name) => typeof name === "string") : void 0;
858
+ const parentCtx = host.parentAgentContext();
859
+ const roster = (typeof parentCtx?.get === "function" ? parentCtx.get("agentPresets") : void 0) ?? host.cordis.get?.("agentPresets");
860
+ let composedPreset = roster?.composedPreset?.(parentCtx);
861
+ if (roster !== void 0 && composedPreset === void 0 && typeof roster.resolve === "function") composedPreset = await roster.resolve(void 0).then((preset) => preset?.id).catch(() => void 0);
862
+ const delegated = host.delegatedPolicyOverrides();
863
+ const route = typeof requestedModel?.id === "string" && requestedModel.id.length > 0 ? {
864
+ model: requestedModel.id,
865
+ ...typeof requestedModel.provider === "string" && requestedModel.provider.length > 0 ? { provider: requestedModel.provider } : {}
866
+ } : host.parentModelRoute?.();
867
+ const agentOptionsFragment = typeof route?.model === "string" && route.model.length > 0 ? { agentOptions: route } : {};
868
+ const setup = async (childCtx) => {
869
+ const childSession = childCtx.agent?.session;
870
+ if (typeof childSession?.append === "function") {
871
+ if (delegated.sandboxMode !== void 0) childSession.append("sandbox/mode", {
872
+ mode: delegated.sandboxMode,
873
+ source: "delegation"
874
+ });
875
+ if (delegated.approvalPolicy !== void 0) childSession.append("approval/policy", {
876
+ policy: delegated.approvalPolicy,
877
+ source: "delegation"
878
+ });
879
+ }
880
+ if (roster !== void 0) {
881
+ let joined;
882
+ try {
883
+ joined = roster.composeFrom?.(childCtx, parentCtx);
884
+ } catch {
885
+ joined = void 0;
886
+ }
887
+ if (joined === void 0 && composedPreset !== void 0 && typeof roster.mount === "function") try {
888
+ await roster.mount(childCtx, composedPreset);
889
+ joined = composedPreset;
890
+ } catch {
891
+ joined = void 0;
892
+ }
893
+ if (joined === void 0) host.cordis.logger?.warn?.("[pi2dsh] child agent joined no preset composition; on roster-owned surfaces its tools resolve against the empty global layer");
894
+ }
895
+ const restriction = requestedNames !== void 0 ? requestedNames.map(nativeToolNameOf) : options.noTools === "all" || options.noTools === true || options.noTools === "builtin" ? [] : void 0;
896
+ if (restriction !== void 0) {
897
+ const toolsService = typeof childCtx.get === "function" ? childCtx.get("tools") : void 0;
898
+ if (toolsService !== void 0) {
899
+ const known = new Set(childSchemas(toolsService, childCtx).map((schema) => schema.name));
900
+ initialRestrictionDispose = toolsService.restrict({ allow: restriction.filter((name) => known.has(name)) });
901
+ }
902
+ }
903
+ const excluded = Array.isArray(options.excludeTools) ? options.excludeTools.filter((name) => typeof name === "string") : [];
904
+ if (excluded.length > 0) {
905
+ const toolsService = typeof childCtx.get === "function" ? childCtx.get("tools") : void 0;
906
+ if (toolsService !== void 0) {
907
+ const known = new Set(childSchemas(toolsService, childCtx).map((schema) => schema.name));
908
+ toolsService.restrict({ deny: excluded.map(nativeToolNameOf).filter((name) => known.has(name)) });
909
+ }
910
+ }
911
+ if (customTools.length > 0 && options.noTools !== "all") host.registerChildTools(childCtx, customTools);
912
+ if (systemPrompt !== void 0) {
913
+ const prompt = typeof childCtx.get === "function" ? childCtx.get("systemPrompt") : void 0;
914
+ if (prompt === void 0) throw new Error("pi2dsh: createAgentSession() got a system prompt but the DSH composition has no systemPrompt service to carry it");
915
+ prompt.section({
916
+ name: "pi2dsh:subagent-system-prompt",
917
+ order: -1e6,
918
+ text: systemPrompt,
919
+ complete: true
920
+ });
921
+ }
922
+ };
923
+ if (resumeSessionId !== void 0) {
924
+ if (typeof agents.resume !== "function") throw new Error("pi2dsh: reopening a child conversation needs the DSH agent registry's persisted-resume seam, which this composition does not provide");
925
+ handle = await agents.resume({
926
+ resumeSessionId,
927
+ ...agentOptionsFragment,
928
+ setup
929
+ });
930
+ } else handle = await agents.create({
721
931
  sessionId,
722
932
  meta: {
723
933
  cwd: typeof options.cwd === "string" ? options.cwd : host.cwd(),
724
934
  origin: "subagent",
725
935
  delegationDepth: host.parentDelegationDepth() + 1,
726
- ...host.parentSessionId() !== void 0 ? { parentSession: host.parentSessionId() } : {}
936
+ ...host.parentSessionId() !== void 0 ? { parentSession: host.parentSessionId() } : {},
937
+ ...typeof composedPreset === "string" && composedPreset.length > 0 ? { agentPreset: composedPreset } : {}
727
938
  },
728
- ...typeof requestedModel?.id === "string" && requestedModel.id.length > 0 ? { agentOptions: {
729
- model: requestedModel.id,
730
- ...typeof requestedModel.provider === "string" && requestedModel.provider.length > 0 ? { provider: requestedModel.provider } : {}
731
- } } : {}
939
+ ...agentOptionsFragment,
940
+ setup
732
941
  });
733
- if (systemPrompt !== void 0) {
734
- const agentCtx = handle.agent.ctx;
735
- const prompt = typeof agentCtx?.get === "function" ? agentCtx.get("systemPrompt") : void 0;
736
- if (prompt === void 0) throw new Error("pi2dsh: createAgentSession() got a system prompt but the DSH composition has no systemPrompt service to carry it");
737
- prompt.section({
738
- name: "pi2dsh:subagent-system-prompt",
739
- order: -1e6,
740
- text: systemPrompt,
741
- complete: true
742
- });
743
- }
744
942
  } catch (error) {
745
- throw new Error(`pi2dsh: subagent creation needs the DSH host loop (model runtime) to provide the agent factory; this composition cannot run one (${error instanceof Error ? error.message : String(error)})`);
943
+ throw new Error(resumeSessionId !== void 0 ? `pi2dsh: reopening child session ${JSON.stringify(resumeSessionId)} failed — its persisted log may be gone or this composition has no session persistence (${error instanceof Error ? error.message : String(error)})` : `pi2dsh: subagent creation needs the DSH host loop (model runtime) to provide the agent factory; this composition cannot run one (${error instanceof Error ? error.message : String(error)})`);
746
944
  }
747
945
  const tools = [...Array.isArray(options.tools) ? options.tools : [], ...Array.isArray(options.customTools) ? options.customTools : []];
748
946
  const childSession = handle.agent.session;
749
- if (typeof childSession?.append === "function") try {
947
+ if (resumeSessionId === void 0 && typeof childSession?.append === "function") try {
750
948
  childSession.append("subagent/descriptor", {
751
949
  version: 2,
752
950
  mode: "continuable",
@@ -756,7 +954,9 @@ async function createBridgedAgentSession(host, options) {
756
954
  } catch (error) {
757
955
  host.cordis.logger?.warn?.(`[pi2dsh] child session could not record its subagent identity: ${error instanceof Error ? error.message : String(error)}`);
758
956
  }
759
- return { session: new PiBridgedAgentSession(host, handle, tools) };
957
+ const session = new PiBridgedAgentSession(host, handle, tools);
958
+ session.adoptRestriction(initialRestrictionDispose);
959
+ return { session };
760
960
  }
761
961
  //#endregion
762
962
  //#region src/compat/vendor/pi-open-browser.ts
@@ -2892,12 +3092,91 @@ function mountTuiSurfaceAdapter(ctx, packageName, publish, instanceKey) {
2892
3092
  }
2893
3093
  }
2894
3094
  /**
2895
- * dsh-TUI handles `/mcp` locally before it asks DSH's command registry. Until
2896
- * it exposes a reserved-command seam, keep its native status command and make
2897
- * an installed Pi MCP manager reachable as `/pi-mcp`.
3095
+ * dsh-TUI dispatches its LOCAL commands before it asks DSH's command
3096
+ * registry ("locals win on name collisions" — its own commands.ts contract),
3097
+ * so a registry command sharing a local name is unreachable from that
3098
+ * surface. The host command keeps its name; the incoming command gets a
3099
+ * `pi-` source prefix — and ONLY on a composition where dsh-TUI is present;
3100
+ * everywhere else the original name stands.
3101
+ *
3102
+ * The reserved-name list is dsh-TUI's `LOCAL_COMMANDS`, which it exports in
3103
+ * code but not through its package exports map (upstream ask filed). Until
3104
+ * that lands, the list is pinned per generation from the published sources:
3105
+ * 0.8.x reserved only /mcp; 0.9.0 reserves the full set below. Generation is
3106
+ * read off the installed package's public ./package.json subpath.
2898
3107
  */
3108
+ const DSH_TUI_LOCAL_COMMANDS_0_9 = /* @__PURE__ */ new Set([
3109
+ "new",
3110
+ "clear",
3111
+ "compact",
3112
+ "resume",
3113
+ "rename",
3114
+ "rewind",
3115
+ "export",
3116
+ "btw",
3117
+ "trace",
3118
+ "context",
3119
+ "status",
3120
+ "cost",
3121
+ "config",
3122
+ "settings",
3123
+ "doctor",
3124
+ "init",
3125
+ "agents",
3126
+ "activity",
3127
+ "preset",
3128
+ "theme",
3129
+ "lang",
3130
+ "model",
3131
+ "effort",
3132
+ "thinking",
3133
+ "tokens",
3134
+ "provider",
3135
+ "login",
3136
+ "logout",
3137
+ "add-dir",
3138
+ "hooks",
3139
+ "mcp",
3140
+ "skills",
3141
+ "plugins",
3142
+ "update",
3143
+ "audit",
3144
+ "bug",
3145
+ "practice",
3146
+ "review",
3147
+ "release-notes",
3148
+ "vuln-check",
3149
+ "vim",
3150
+ "terminal-setup",
3151
+ "connect",
3152
+ "workspace",
3153
+ "help",
3154
+ "tips",
3155
+ "exit",
3156
+ "quit",
3157
+ "q",
3158
+ "deepseek"
3159
+ ]);
3160
+ const DSH_TUI_LOCAL_COMMANDS_LEGACY = /* @__PURE__ */ new Set(["mcp"]);
3161
+ /** The reserved set for one installed dsh-tui version (undefined = unknown,
3162
+ * assume the current generation). */
3163
+ function tuiLocalCommandsForVersion(version) {
3164
+ if (version === void 0) return DSH_TUI_LOCAL_COMMANDS_0_9;
3165
+ const [major = 0, minor = 0] = version.split(".").map((part) => Number.parseInt(part, 10));
3166
+ return major > 0 || minor >= 9 ? DSH_TUI_LOCAL_COMMANDS_0_9 : DSH_TUI_LOCAL_COMMANDS_LEGACY;
3167
+ }
3168
+ let cachedTuiLocalCommands;
3169
+ function dshTuiLocalCommands() {
3170
+ if (cachedTuiLocalCommands !== void 0) return cachedTuiLocalCommands;
3171
+ let version;
3172
+ try {
3173
+ version = createRequire(import.meta.url)("@deepseek-harness-tui/dsh-tui/package.json").version;
3174
+ } catch {}
3175
+ cachedTuiLocalCommands = tuiLocalCommandsForVersion(version);
3176
+ return cachedTuiLocalCommands;
3177
+ }
2899
3178
  function commandNameForDshTui(name, tuiAvailable) {
2900
- return tuiAvailable && name === "mcp" ? "pi-mcp" : name;
3179
+ return tuiAvailable && dshTuiLocalCommands().has(name) ? `pi-${name}` : name;
2901
3180
  }
2902
3181
  //#endregion
2903
3182
  //#region src/runtime.ts
@@ -5172,6 +5451,197 @@ async function discoverProviderModels(ctx, state, name, value) {
5172
5451
  if (state.shared.providerModelDiscoveries.get(name) === task) state.shared.providerModelDiscoveries.delete(name);
5173
5452
  }
5174
5453
  }
5454
+ /**
5455
+ * Run one provider's own OAuth login end to end — the shared spine behind
5456
+ * the /login command and the DSH authorization-seam flow. Owns the
5457
+ * host-wide single-flight (the fixed callback port), the short-link
5458
+ * publication, and the post-login route + catalog refresh.
5459
+ * @returns the user-facing summary line.
5460
+ */
5461
+ async function runProviderLogin(ctx, state, providerId, config, ui, externalSignal) {
5462
+ const oauthName = config.oauth?.name ?? providerId;
5463
+ const superseded = await supersedeActiveLogin(state);
5464
+ if (superseded !== void 0) ui.notify(`Cancelled the ${superseded} login that was still waiting.`);
5465
+ const active = {
5466
+ providerName: oauthName,
5467
+ controller: new AbortController(),
5468
+ finished: Promise.resolve(),
5469
+ published: []
5470
+ };
5471
+ if (externalSignal !== void 0) {
5472
+ if (externalSignal.aborted) active.controller.abort();
5473
+ else externalSignal.addEventListener("abort", () => active.controller.abort(), { once: true });
5474
+ }
5475
+ const attempt = loginPiProvider({
5476
+ open: openBrowser,
5477
+ shorten: (url) => {
5478
+ const path = publishAuthorization(url);
5479
+ const web = ctx.get("webServer");
5480
+ if (path === void 0 || web?.port === void 0) return void 0;
5481
+ active.published.push(path);
5482
+ return `http://${web.host === "0.0.0.0" || web.host === void 0 ? "127.0.0.1" : web.host}:${web.port}${path}`;
5483
+ },
5484
+ providerId,
5485
+ providerName: oauthName,
5486
+ providerConfig: config,
5487
+ store: oauthStoreOf(state),
5488
+ ui,
5489
+ signal: active.controller.signal
5490
+ });
5491
+ active.finished = attempt.then(() => void 0, () => void 0);
5492
+ state.shared.activeLogin = active;
5493
+ try {
5494
+ await attempt;
5495
+ } finally {
5496
+ for (const path of active.published) revokeAuthorization(path);
5497
+ if (state.shared.activeLogin === active) state.shared.activeLogin = void 0;
5498
+ }
5499
+ const declared = await ensureLoggedInProviderRoute(ctx, state, providerId, config);
5500
+ const discovered = await discoverProviderModels(ctx, state, providerId, config) ?? (declared ? await llmOf(ctx)?.listModels(providerId).then((list) => list.length).catch(() => void 0) : void 0);
5501
+ const models = discovered === void 0 ? "" : `; ${discovered} models available`;
5502
+ ui.notify(`Logged in to ${oauthName}${models}`);
5503
+ return `Logged in to ${oauthName}${models}`;
5504
+ }
5505
+ /**
5506
+ * Project one provider's OAuth login onto DSH's official authorization seam
5507
+ * (`ctx.authorization`, 0.1.1 line). The flow runs the SAME login spine as
5508
+ * /login; the DSH sign-in surface supplies the interaction. On hosts without
5509
+ * the seam (rc.8 line) this is a no-op and /login stays the only entry.
5510
+ *
5511
+ * The credential itself stays in the bridge's Pi-format store — the DSH
5512
+ * credential record written at the end is the seam's commit witness (the
5513
+ * service verifies it observed a record update for the flow's key), and the
5514
+ * official sign-out (`deleteRecord`) is mirrored back into the Pi store by
5515
+ * the watcher below.
5516
+ *
5517
+ * Same id under another scope (the official llm-pi-ai catalog registers
5518
+ * sign-ins for its whole catalog) is NOT a conflict: flows are keyed by
5519
+ * scope/id exactly so registrars can coexist, and the credential spaces
5520
+ * really are separate — signing in through the official flow lands a record
5521
+ * only llm-pi-ai routes can use, while this flow authorizes the routes the
5522
+ * bridge actually serves (package transports and logged-in builtins).
5523
+ * Standing down here would leave those routes unreachable from the native
5524
+ * surface. The label carries "(pi2dsh)" so a future surface shows whose
5525
+ * sign-in each entry is. Only our own exact key is defended: a collision on
5526
+ * `pi2dsh/<id>` throws in registerFlow and degrades to /login-only.
5527
+ *
5528
+ * Composition order is not ours to assume: the stock 0.1.1 compositions ship
5529
+ * the authorization package without composing it, so the service may appear
5530
+ * (or bounce) at any time after this provider registers. The official
5531
+ * pattern — the one llm-pi-ai itself uses — is `ctx.inject(['authorization',
5532
+ * ...], cb)`: cordis mounts the callback when the services are present,
5533
+ * disposes everything it registered when they leave, and mounts it again if
5534
+ * they return. The scope hangs off the CALLING ctx, so a flow projected for
5535
+ * a package's provider dies with that package, and a built-in entry's flow
5536
+ * dies with the engine.
5537
+ */
5538
+ function maybeProjectAuthorizationFlow(ctx, state, providerId) {
5539
+ const shared = state.shared;
5540
+ shared.authorizationArmedIds ??= /* @__PURE__ */ new Set();
5541
+ if (shared.authorizationArmedIds.has(providerId)) return;
5542
+ const inject = ctx.inject;
5543
+ if (typeof inject !== "function") return;
5544
+ shared.authorizationArmedIds.add(providerId);
5545
+ inject.call(ctx, ["authorization", "credentials"], (scope) => {
5546
+ projectAuthorizationFlow(scope, state, providerId);
5547
+ });
5548
+ }
5549
+ /** The per-mount half: services are present on `scope`, register the flow and
5550
+ * this provider's sign-out mirror. Everything registered here is disposed by
5551
+ * cordis with the scope. */
5552
+ async function projectAuthorizationFlow(scope, state, providerId) {
5553
+ const shared = state.shared;
5554
+ const ctx = scope;
5555
+ const authorization = optionalService(ctx, "authorization");
5556
+ const credentials = optionalService(ctx, "credentials");
5557
+ if (authorization === void 0 || credentials === void 0) return;
5558
+ try {
5559
+ const { credentialKey, isCredentialKeySegment } = await import("@deepseek-ai/dsh-credentials");
5560
+ if (credentialKey === void 0 || isCredentialKeySegment === void 0) return;
5561
+ if (!isCredentialKeySegment(providerId)) {
5562
+ logger(ctx).warn(`[pi2dsh] provider ${JSON.stringify(providerId)} cannot appear on the DSH sign-in surface (its id is outside the credential-key grammar); /login still works`);
5563
+ return;
5564
+ }
5565
+ const canonicalOf = () => shared.providers.get(providerId) ?? {};
5566
+ const labelOf = () => canonicalOf().oauth?.name ?? providerId;
5567
+ const key = credentialKey("pi2dsh", providerId);
5568
+ authorization.registerFlow({
5569
+ key,
5570
+ label: `${labelOf()} (pi2dsh)`,
5571
+ methods: [{
5572
+ id: "oauth",
5573
+ label: `Sign in to ${labelOf()}`
5574
+ }],
5575
+ run: async (session) => {
5576
+ const ui = {
5577
+ notify: (message) => {
5578
+ session.notify({ message: String(message) });
5579
+ },
5580
+ input: async (title, _placeholder, promptSignal) => {
5581
+ try {
5582
+ return await session.prompt({
5583
+ kind: "text",
5584
+ message: String(title),
5585
+ ...promptSignal instanceof AbortSignal ? { signal: promptSignal } : {}
5586
+ });
5587
+ } catch (error) {
5588
+ if (promptSignal instanceof AbortSignal && promptSignal.aborted) return void 0;
5589
+ throw error;
5590
+ }
5591
+ },
5592
+ select: async (title, options, promptSignal) => {
5593
+ try {
5594
+ return await session.prompt({
5595
+ kind: "select",
5596
+ message: String(title),
5597
+ options: options.map((option) => ({
5598
+ id: String(option),
5599
+ label: String(option)
5600
+ })),
5601
+ ...promptSignal instanceof AbortSignal ? { signal: promptSignal } : {}
5602
+ });
5603
+ } catch (error) {
5604
+ if (promptSignal instanceof AbortSignal && promptSignal.aborted) return void 0;
5605
+ throw error;
5606
+ }
5607
+ },
5608
+ deviceCode: async (title, detail) => {
5609
+ session.notify({
5610
+ message: String(title),
5611
+ code: String(detail)
5612
+ });
5613
+ }
5614
+ };
5615
+ const canonical = canonicalOf();
5616
+ if (!providerSupportsOAuth(canonical)) throw new Error(`${providerId} no longer supports OAuth login`);
5617
+ await runProviderLogin(ctx, state, providerId, canonical, ui, session.signal);
5618
+ await credentials.modifyRecord(key, async () => ({
5619
+ kind: "grant",
5620
+ payload: {
5621
+ provider: providerId,
5622
+ managedBy: "pi2dsh"
5623
+ }
5624
+ }));
5625
+ }
5626
+ });
5627
+ ctx.on("credentials/record-updated", (eventKey) => {
5628
+ (async () => {
5629
+ try {
5630
+ if (String(eventKey) !== String(key)) return;
5631
+ if (await credentials.readRecord(key) !== void 0) return;
5632
+ await oauthStoreOf(state).delete(providerId);
5633
+ const canonical = shared.providers.get(providerId);
5634
+ if (canonical !== void 0 && !providerCarriesTransport(canonical)) retireSharedProviderRoute(shared, providerId);
5635
+ logger(ctx).info(`[pi2dsh] signed out of ${providerId}: the DSH credential record was deleted, so the bridge's stored login was removed`);
5636
+ } catch (error) {
5637
+ logger(ctx).warn(`[pi2dsh] sign-out mirror failed: ${error instanceof Error ? error.message : String(error)}`);
5638
+ }
5639
+ })();
5640
+ });
5641
+ } catch (error) {
5642
+ logger(ctx).warn(`[pi2dsh] could not project ${JSON.stringify(providerId)} onto the DSH sign-in surface: ${error instanceof Error ? error.message : String(error)}; /login still works`);
5643
+ }
5644
+ }
5175
5645
  function registerLoginCommand(ctx, state) {
5176
5646
  registerCommand(ctx, state, {
5177
5647
  name: "login",
@@ -5187,49 +5657,7 @@ function registerLoginCommand(ctx, state) {
5187
5657
  const providerId = resolveOfferedChoice(answer, oauthProviders);
5188
5658
  const config = providerId === void 0 ? void 0 : state.providers.get(providerId);
5189
5659
  if (providerId === void 0 || config === void 0 || !providerSupportsOAuth(config)) throw new Error(`unknown OAuth provider ${JSON.stringify(answer)}; available: ${oauthProviders.join(", ")}`);
5190
- const oauthName = config.oauth?.name ?? providerId;
5191
- const commandSignal = commandContext.signal;
5192
- const superseded = await supersedeActiveLogin(state);
5193
- if (superseded !== void 0) ui.notify(`Cancelled the ${superseded} login that was still waiting.`);
5194
- const active = {
5195
- providerName: oauthName,
5196
- controller: new AbortController(),
5197
- finished: Promise.resolve(),
5198
- published: []
5199
- };
5200
- if (commandSignal !== void 0) {
5201
- if (commandSignal.aborted) active.controller.abort();
5202
- else commandSignal.addEventListener("abort", () => active.controller.abort(), { once: true });
5203
- }
5204
- const attempt = loginPiProvider({
5205
- open: openBrowser,
5206
- shorten: (url) => {
5207
- const path = publishAuthorization(url);
5208
- const web = ctx.get("webServer");
5209
- if (path === void 0 || web?.port === void 0) return void 0;
5210
- active.published.push(path);
5211
- return `http://${web.host === "0.0.0.0" || web.host === void 0 ? "127.0.0.1" : web.host}:${web.port}${path}`;
5212
- },
5213
- providerId,
5214
- providerName: oauthName,
5215
- providerConfig: config,
5216
- store: oauthStoreOf(state),
5217
- ui,
5218
- signal: active.controller.signal
5219
- });
5220
- active.finished = attempt.then(() => void 0, () => void 0);
5221
- state.shared.activeLogin = active;
5222
- try {
5223
- await attempt;
5224
- } finally {
5225
- for (const path of active.published) revokeAuthorization(path);
5226
- if (state.shared.activeLogin === active) state.shared.activeLogin = void 0;
5227
- }
5228
- const declared = await ensureLoggedInProviderRoute(ctx, state, providerId, config);
5229
- const discovered = await discoverProviderModels(ctx, state, providerId, config) ?? (declared ? await llmOf(ctx)?.listModels(providerId).then((list) => list.length).catch(() => void 0) : void 0);
5230
- const models = discovered === void 0 ? "" : `; ${discovered} models available`;
5231
- ui.notify(`Logged in to ${oauthName}${models}`);
5232
- return `Logged in to ${oauthName}${models}`;
5660
+ return await runProviderLogin(ctx, state, providerId, config, ui, commandContext.signal);
5233
5661
  }
5234
5662
  });
5235
5663
  }
@@ -5292,6 +5720,50 @@ function registerTool(ctx, state, tool) {
5292
5720
  state.toolDisposers.set(tool.name, dispose);
5293
5721
  if (isKnownImageTool(state.packageName, tool.name)) state.shared.browserSurfaces?.registerImageTool(tool.name);
5294
5722
  }
5723
+ /**
5724
+ * Register ONE Pi custom tool into a child agent's scope (createAgentSession
5725
+ * customTools). Same translation as {@link registerTool}'s definition, but:
5726
+ * the registration goes through the CHILD ctx (a scoped registration that
5727
+ * unwinds with the child, invisible to the parent and siblings), and the
5728
+ * package's own Pi tool ledger is untouched — a child session's custom tools
5729
+ * belong to that session in Pi, not to the extension's registered set.
5730
+ * Partial-result updates have no Pi-side consumer on this path yet (the
5731
+ * façade projects durable events only), so the update callback is a no-op —
5732
+ * never a fake dispatch.
5733
+ */
5734
+ function registerChildPiTool(childCtx, state, tool) {
5735
+ const normalized = normalizeToolSchema(tool.parameters);
5736
+ for (const warning of normalized.warnings) logger(childCtx).warn(`[pi2dsh] subagent tool ${tool.name}: ${warning}`);
5737
+ const definition = {
5738
+ name: tool.name,
5739
+ description: tool.description,
5740
+ parameters: normalized.schema,
5741
+ output: {
5742
+ schema: {},
5743
+ render: (_args, value) => value.content,
5744
+ presentationMeta: (_args, value) => jsonValue(value.details)
5745
+ },
5746
+ isConcurrencySafe: () => tool.executionMode === "parallel",
5747
+ async execute(args, exec) {
5748
+ const agent = exec.agent;
5749
+ const prepared = validateToolArguments({
5750
+ name: tool.name,
5751
+ parameters: tool.parameters
5752
+ }, {
5753
+ name: tool.name,
5754
+ arguments: tool.prepareArguments?.(cloneJson(args)) ?? args
5755
+ });
5756
+ const result = await normalizeToolResultForDsh(childCtx, await runInPiRuntime(state, agent, () => tool.execute(String(exec.callId), prepared, exec.signal, () => {}, contextFor(childCtx, state, agent, exec.signal))));
5757
+ if (result.terminate === true) exec.concludeTurn();
5758
+ if (result.isError === true) {
5759
+ const message = textBlocks(result.content).map((block) => block.text).filter(Boolean).join("\n");
5760
+ throw new Error(message || `Pi tool ${tool.name} failed`);
5761
+ }
5762
+ return result;
5763
+ }
5764
+ };
5765
+ childCtx.tools.register(definition);
5766
+ }
5295
5767
  function unregisterTool(state, name) {
5296
5768
  const dispose = state.toolDisposers.get(name);
5297
5769
  if (dispose === void 0) return false;
@@ -5540,9 +6012,10 @@ function normalizedPiCommandName(piName) {
5540
6012
  return normalized.length > 0 ? normalized : "pi-command";
5541
6013
  }
5542
6014
  function dshCommandName(ctx, state, piName) {
5543
- const name = commandNameForDshTui(normalizedPiCommandName(piName), state.tuiSurfaces?.available === true || optionalService(ctx, "tuiScenes") !== void 0);
6015
+ const validName = normalizedPiCommandName(piName);
6016
+ const name = commandNameForDshTui(validName, state.tuiSurfaces?.available === true || optionalService(ctx, "tuiScenes") !== void 0);
5544
6017
  if (name !== piName) {
5545
- const reason = name === "pi-mcp" ? "because dsh-TUI reserves /mcp for its native MCP status view" : "to satisfy DSH command naming";
6018
+ const reason = name === `pi-${validName}` ? `because dsh-TUI reserves /${validName} for its own local command (locals win on name collisions)` : "to satisfy DSH command naming";
5546
6019
  logger(ctx).warn(`[pi2dsh] Pi command /${piName} registered as /${name} ${reason}`);
5547
6020
  }
5548
6021
  return name;
@@ -5733,6 +6206,7 @@ function createPiApi(ctx, state) {
5733
6206
  const canonical = state.shared.providers.get(name) ?? value;
5734
6207
  if (providerSupportsOAuth(canonical)) {
5735
6208
  ensureLoginCommand(ctx, state);
6209
+ maybeProjectAuthorizationFlow(ctx, state, name);
5736
6210
  logger(ctx).info(`[pi2dsh] Pi provider ${JSON.stringify(name)} supports OAuth — log in with /login ${name}`);
5737
6211
  }
5738
6212
  if (registerSharedProviderRoute(ctx, state, name, canonical) !== void 0) {
@@ -6105,6 +6579,7 @@ async function applyPiPackage(ctx, options) {
6105
6579
  shared.providerBuiltins.set(provider.id, config);
6106
6580
  recomposeSharedProvider(shared, provider.id);
6107
6581
  }
6582
+ maybeProjectAuthorizationFlow(ctx, state, provider.id);
6108
6583
  }
6109
6584
  ensureLoginCommand(ctx, state);
6110
6585
  for (const [name, config] of [...state.providers]) try {
@@ -6232,7 +6707,36 @@ async function applyPiPackage(ctx, options) {
6232
6707
  deliver: (agent, message, mode) => deliverAgentMessage(agent, message, mode),
6233
6708
  messageFromSessionEvent: (event) => messageFromSessionEvent(ctx, event),
6234
6709
  messageSource: state.messageSource,
6235
- packageName: state.packageName
6710
+ packageName: state.packageName,
6711
+ parentAgentContext: () => currentAgent(state)?.ctx,
6712
+ registerChildTools: (childCtx, tools) => {
6713
+ for (const tool of tools) registerChildPiTool(childCtx, state, tool);
6714
+ },
6715
+ delegatedPolicyOverrides: () => {
6716
+ const parent = currentAgent(state);
6717
+ const sandboxPolicy = typeof parent?.ctx?.get === "function" ? parent.ctx.get("sandboxPolicy") : void 0;
6718
+ const approval = typeof parent?.ctx?.get === "function" ? parent.ctx.get("approval") : void 0;
6719
+ const sandboxMode = sandboxPolicy?.overrideOf?.(parent?.session);
6720
+ return {
6721
+ ...sandboxMode !== void 0 ? { sandboxMode } : {},
6722
+ ...approval !== void 0 ? { approvalPolicy: "never" } : {}
6723
+ };
6724
+ },
6725
+ sessionManagerFor: (session) => {
6726
+ const typed = session;
6727
+ const cwd = typeof typed.meta?.cwd === "string" ? typed.meta.cwd : cwdOf(currentAgent(state));
6728
+ return state.bridge.readonlySessionManager(session, cwd);
6729
+ },
6730
+ resumeSessionIdFor: (file) => state.bridge.sessionIdOfArchiveFile(file),
6731
+ parentModelRoute: () => {
6732
+ const parent = currentAgent(state);
6733
+ const model = parent?.options?.model;
6734
+ if (typeof model !== "string" || model.length === 0) return void 0;
6735
+ return {
6736
+ model,
6737
+ ...typeof parent?.options?.provider === "string" && parent.options.provider.length > 0 ? { provider: parent.options.provider } : {}
6738
+ };
6739
+ }
6236
6740
  });
6237
6741
  await registerPromptCommands(ctx, state, rootDir, options.manifest);
6238
6742
  const onExtensionError = (failure) => logger(ctx).warn(`[pi2dsh] extension entry failed and was skipped (matching Pi's per-extension error isolation): ${failure}`);
@@ -6286,4 +6790,4 @@ const runtimeInternals = {
6286
6790
  //#endregion
6287
6791
  export { registerVisionCompanions as a, overlayProviderConfig as i, mergeProviderRegistration as n, runtimeInternals as o, normalizeToolSchema as r, applyPiPackage as t };
6288
6792
 
6289
- //# sourceMappingURL=runtime-BlbKWw7W.mjs.map
6793
+ //# sourceMappingURL=runtime-Bjc6ijZg.mjs.map