apex-code 0.0.1-alpha.6 → 0.0.1-alpha.7

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 (29) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/agent-session.d.ts +17 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +28 -0
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/permissions/gate.d.ts +7 -0
  7. package/dist/core/permissions/gate.d.ts.map +1 -1
  8. package/dist/core/permissions/gate.js.map +1 -1
  9. package/dist/core/permissions/startup.d.ts +12 -0
  10. package/dist/core/permissions/startup.d.ts.map +1 -1
  11. package/dist/core/permissions/startup.js +12 -4
  12. package/dist/core/permissions/startup.js.map +1 -1
  13. package/dist/main.d.ts.map +1 -1
  14. package/dist/main.js +1 -0
  15. package/dist/main.js.map +1 -1
  16. package/dist/modes/interactive/components/footer.d.ts +3 -0
  17. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  18. package/dist/modes/interactive/components/footer.js +13 -0
  19. package/dist/modes/interactive/components/footer.js.map +1 -1
  20. package/dist/modes/interactive/components/settings-selector.d.ts +13 -0
  21. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/settings-selector.js +96 -0
  23. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  24. package/dist/modes/interactive/interactive-mode.d.ts +11 -0
  25. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  26. package/dist/modes/interactive/interactive-mode.js +62 -3
  27. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  28. package/npm-shrinkwrap.json +5 -5
  29. package/package.json +2 -2
@@ -19,6 +19,13 @@ export interface PermissionGateOptions {
19
19
  getContract: (toolName: string) => ToolContract | undefined;
20
20
  store: PermissionRuleStore;
21
21
  getMode: () => PermissionMode | Promise<PermissionMode>;
22
+ /**
23
+ * The `--permission-mode` value, when one was passed. Not used to decide
24
+ * anything here — `getMode` already folds it in. It is carried so a UI that
25
+ * offers to change the mode can tell the user their write is outranked
26
+ * (permissions/startup.ts, resolveEffectiveModeWithOrigin).
27
+ */
28
+ flagMode?: PermissionMode;
22
29
  /** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */
23
30
  responder?: PermissionResponder;
24
31
  /** Resolves a responder at call time because the interactive UI binds after session construction. */
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtE,MAAM,WAAW,qBAAqB;IACrC,2IAA2I;IAC3I,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,GAAG,SAAS,CAAC;IAC5D,KAAK,EAAE,mBAAmB,CAAC;IAC3B,OAAO,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxD,yFAAyF;IACzF,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,qGAAqG;IACrG,YAAY,CAAC,EAAE,MAAM,mBAAmB,GAAG,SAAS,CAAC;CACrD;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,oHAAkH;AAClH,wBAAsB,gBAAgB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,qBAAqB,GAC5B,CAAC,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAKrG","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t});\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `${toolName} was declined.` };\n\t}\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: \"allow\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtE,MAAM,WAAW,qBAAqB;IACrC,2IAA2I;IAC3I,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,GAAG,SAAS,CAAC;IAC5D,KAAK,EAAE,mBAAmB,CAAC;IAC3B,OAAO,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,yFAAyF;IACzF,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,qGAAqG;IACrG,YAAY,CAAC,EAAE,MAAM,mBAAmB,GAAG,SAAS,CAAC;CACrD;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,oHAAkH;AAClH,wBAAsB,gBAAgB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,qBAAqB,GAC5B,CAAC,OAAO,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAKrG","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/**\n\t * The `--permission-mode` value, when one was passed. Not used to decide\n\t * anything here — `getMode` already folds it in. It is carried so a UI that\n\t * offers to change the mode can tell the user their write is outranked\n\t * (permissions/startup.ts, resolveEffectiveModeWithOrigin).\n\t */\n\tflagMode?: PermissionMode;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t});\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `${toolName} was declined.` };\n\t}\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: \"allow\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,mBAAmB,EAAqB,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAmB/C,SAAS,gBAAgB,CAAC,QAAsB,EAAE,QAAgB,EAAE,WAA+B,EAAU;IAC5G,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAChF,OAAO,GAAG,QAAQ,4DAA4D,CAAC;AAAA,CAC/E;AAED,oHAAkH;AAClH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,QAAgB,EAChB,MAAe,EACf,OAA8B,EACN;IACxB,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACpE,MAAM,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAC;IACjC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAChD,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtF,OAAO;YACN,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,iDAAiD,OAAO,sBAAsB,QAAQ,GAAG;SACjG,CAAC;IACH,CAAC;IACD,MAAM,cAAc,GAAG,iBAAiB,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAe,CAAC,CAAC;IAC1F,MAAM,UAAU,GAAG,eAAe,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,cAAc,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;IAEnG,IAAI,UAAU,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAE7D,IAAI,UAAU,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QACpC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,CAAC;IACpG,CAAC;IAED,MAAM;IACN,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,EAAE,IAAI,OAAO,CAAC,SAAS,CAAC;IAChE,IAAI,CAAC,SAAS,EAAE,CAAC;QAChB,OAAO;YACN,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,GAAG,QAAQ,oEAAoE;SACvF,CAAC;IACH,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,MAAe,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC;QAClC,QAAQ;QACR,WAAW,EAAE,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,OAAO,QAAQ,EAAE;KAClF,CAAC,CAAC;IACH,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACnB,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,gBAAgB,EAAE,CAAC;IAC7D,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;YACzB,IAAI,EAAE,UAAU;YAChB,WAAW,EAAE,SAAS;YACtB,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;SAClE,CAAC,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAAA,CACxB;AAED,uFAAuF;AACvF,MAAM,UAAU,oBAAoB,CACnC,OAA8B,EACwE;IACtG,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACtE,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAAA,CAC7E,CAAC;AAAA,CACF","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t});\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `${toolName} was declined.` };\n\t}\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: \"allow\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/core/permissions/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,mBAAmB,EAAqB,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA0B/C,SAAS,gBAAgB,CAAC,QAAsB,EAAE,QAAgB,EAAE,WAA+B,EAAU;IAC5G,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAChF,OAAO,GAAG,QAAQ,4DAA4D,CAAC;AAAA,CAC/E;AAED,oHAAkH;AAClH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,QAAgB,EAChB,MAAe,EACf,OAA8B,EACN;IACxB,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACpE,MAAM,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAC;IACjC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;IAChD,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtF,OAAO;YACN,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,iDAAiD,OAAO,sBAAsB,QAAQ,GAAG;SACjG,CAAC;IACH,CAAC;IACD,MAAM,cAAc,GAAG,iBAAiB,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAe,CAAC,CAAC;IAC1F,MAAM,UAAU,GAAG,eAAe,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,cAAc,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;IAEnG,IAAI,UAAU,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAE7D,IAAI,UAAU,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QACpC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,CAAC;IACpG,CAAC;IAED,MAAM;IACN,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,EAAE,IAAI,OAAO,CAAC,SAAS,CAAC;IAChE,IAAI,CAAC,SAAS,EAAE,CAAC;QAChB,OAAO;YACN,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,GAAG,QAAQ,oEAAoE;SACvF,CAAC;IACH,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,MAAe,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC;QAClC,QAAQ;QACR,WAAW,EAAE,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,OAAO,QAAQ,EAAE;KAClF,CAAC,CAAC;IACH,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACnB,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,gBAAgB,EAAE,CAAC;IAC7D,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;YACzB,IAAI,EAAE,UAAU;YAChB,WAAW,EAAE,SAAS;YACtB,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;SAClE,CAAC,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAAA,CACxB;AAED,uFAAuF;AACvF,MAAM,UAAU,oBAAoB,CACnC,OAA8B,EACwE;IACtG,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACtE,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAAA,CAC7E,CAAC;AAAA,CACF","sourcesContent":["/**\n * The permission gate: composes rule resolution (rules.ts), mode overlay\n * (modes.ts), and interactive escalation (responder.ts) into a single decision,\n * and adapts that decision to the `beforeToolCall` seam `agent-core` already\n * exposes (packages/agent/src/agent-loop.ts). No new hook, no agent-core change.\n *\n * Tool execution has exactly one call site downstream of this hook\n * (`executePreparedToolCall`, fed only by the function that runs `beforeToolCall`),\n * so wiring this in once at session setup is sufficient for \"every registered tool\n * passes the gate\" to be true by construction — the universal-gate test (invariant\n * 2) verifies this property holds, it does not have to work to make it hold.\n */\n\nimport type { BeforeToolCallContext, BeforeToolCallResult } from \"apex-code-agent-core\";\nimport { resolveToolContract, type ToolContract } from \"../tools/contract.ts\";\nimport { resolveWithMode } from \"./modes.ts\";\nimport type { PermissionResponder } from \"./responder.ts\";\nimport { resolvePermission } from \"./rules.ts\";\nimport type { PermissionMode, PermissionRuleStore } from \"./store.ts\";\n\nexport interface PermissionGateOptions {\n\t/** Resolve a tool's contract by name. Foreign tools with no contract get UNCLASSIFIED, never rejected or silently defaulted (ADR 0010). */\n\tgetContract: (toolName: string) => ToolContract | undefined;\n\tstore: PermissionRuleStore;\n\tgetMode: () => PermissionMode | Promise<PermissionMode>;\n\t/**\n\t * The `--permission-mode` value, when one was passed. Not used to decide\n\t * anything here — `getMode` already folds it in. It is carried so a UI that\n\t * offers to change the mode can tell the user their write is outranked\n\t * (permissions/startup.ts, resolveEffectiveModeWithOrigin).\n\t */\n\tflagMode?: PermissionMode;\n\t/** Absent in a non-interactive session: an `ask` resolution then fails closed (deny). */\n\tresponder?: PermissionResponder;\n\t/** Resolves a responder at call time because the interactive UI binds after session construction. */\n\tgetResponder?: () => PermissionResponder | undefined;\n}\n\nexport interface GateDecision {\n\tblock: boolean;\n\treason?: string;\n}\n\nfunction describeDecision(contract: ToolContract, toolName: string, ruleContent: string | undefined): string {\n\tif (ruleContent !== undefined) return contract.permission.describe(ruleContent);\n\treturn `${toolName} is not permitted by the current permission configuration.`;\n}\n\n/** Pure decision function, independent of the beforeToolCall adapter shape below — the part under direct test. */\nexport async function evaluateToolCall(\n\ttoolName: string,\n\tparams: unknown,\n\toptions: PermissionGateOptions,\n): Promise<GateDecision> {\n\tconst contract = resolveToolContract(options.getContract, toolName);\n\tconst spec = contract.permission;\n\tconst snapshot = await options.store.snapshot();\n\tif (snapshot.errors.length > 0) {\n\t\tconst sources = [...new Set(snapshot.errors.map((entry) => entry.source))].join(\", \");\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `Permission configuration could not be loaded (${sources}); refusing to run ${toolName}.`,\n\t\t};\n\t}\n\tconst ruleResolution = resolvePermission(snapshot.rules, toolName, spec, params as never);\n\tconst resolution = resolveWithMode(await options.getMode(), ruleResolution, contract.capabilities);\n\n\tif (resolution.behavior === \"allow\") return { block: false };\n\n\tif (resolution.behavior === \"deny\") {\n\t\treturn { block: true, reason: describeDecision(contract, toolName, resolution.rule?.ruleContent) };\n\t}\n\n\t// ask\n\tconst responder = options.getResponder?.() ?? options.responder;\n\tif (!responder) {\n\t\treturn {\n\t\t\tblock: true,\n\t\t\treason: `${toolName} requires approval, and no responder is available in this session.`,\n\t\t};\n\t}\n\tconst ruleForCall = spec.ruleForCall(params as never);\n\tconst answer = await responder.ask({\n\t\ttoolName,\n\t\tdescription: ruleForCall !== null ? spec.describe(ruleForCall) : `Run ${toolName}`,\n\t});\n\tif (!answer.allow) {\n\t\treturn { block: true, reason: `${toolName} was declined.` };\n\t}\n\tif (answer.persist && ruleForCall !== null) {\n\t\tawait options.store.apply({\n\t\t\ttype: \"addRules\",\n\t\t\tdestination: \"session\",\n\t\t\trules: [{ toolName, behavior: \"allow\", ruleContent: ruleForCall }],\n\t\t});\n\t}\n\treturn { block: false };\n}\n\n/** Adapts evaluateToolCall() to the beforeToolCall seam agent-core already exposes. */\nexport function createPermissionGate(\n\toptions: PermissionGateOptions,\n): (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined> {\n\treturn async ({ toolCall, args }) => {\n\t\tconst decision = await evaluateToolCall(toolCall.name, args, options);\n\t\treturn decision.block ? { block: true, reason: decision.reason } : undefined;\n\t};\n}\n"]}
@@ -24,10 +24,22 @@ export declare function resolvePermissionModeForStartup(options: {
24
24
  interactive: boolean;
25
25
  requestedMode: PermissionMode | undefined;
26
26
  }): PermissionModeStartupResult;
27
+ /** Where an effective mode came from. `"default"` means nothing set one. */
28
+ export type PermissionModeOrigin = "flag" | WritablePermissionSource | "default";
29
+ export interface EffectiveModeResolution {
30
+ mode: PermissionMode;
31
+ origin: PermissionModeOrigin;
32
+ }
27
33
  /**
28
34
  * The `--permission-mode` flag outranks every persisted mode, including a project
29
35
  * rule a team committed — a deliberate per-run operator decision should not be
30
36
  * silently overridden by checked-in config (ADR 0004).
37
+ *
38
+ * Returns the origin alongside the mode so a caller that offers to *change* the
39
+ * mode can tell whether its write would actually take effect. A settings toggle
40
+ * writing `user` while `local` or the flag outranks it is a silent no-op, which is
41
+ * the one failure a UI here must not have.
31
42
  */
43
+ export declare function resolveEffectiveModeWithOrigin(flagMode: PermissionMode | undefined, modesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>): EffectiveModeResolution;
32
44
  export declare function resolveEffectiveMode(flagMode: PermissionMode | undefined, modesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>): PermissionMode;
33
45
  //# sourceMappingURL=startup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/startup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAoB,KAAK,cAAc,EAAE,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAElG,MAAM,MAAM,2BAA2B,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9G;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACxD,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,cAAc,GAAG,SAAS,CAAC;CAC1C,GAAG,2BAA2B,CAW9B;AAYD;;;;GAIG;AACH,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,cAAc,GAAG,SAAS,EACpC,aAAa,EAAE,WAAW,CAAC,wBAAwB,EAAE,cAAc,CAAC,GAClE,cAAc,CAUhB","sourcesContent":["/**\n * Startup-time permission mode resolution (ADR 0004). A misconfigured\n * non-interactive run should fail fast, at startup, with an actionable message —\n * not deny every tool call mid-run in a way that looks like a bug (see the ADR's\n * \"ask fails closed, and a session that cannot ask refuses to start\").\n */\n\nimport { PERMISSION_MODES, type PermissionMode, type WritablePermissionSource } from \"./store.ts\";\n\nexport type PermissionModeStartupResult = { ok: true; mode: PermissionMode } | { ok: false; message: string };\n\n/**\n * `requestedMode` is assumed already validated against `PERMISSION_MODES` (args.ts\n * rejects an unknown value at parse time) — this only decides whether a session may\n * start *without* one. Interactive sessions may: an `ask` there has a human to\n * resolve it. Non-interactive sessions may not: with no responder, `ask` fails\n * closed to `deny`, and discovering that per-call, mid-run, is the exact bad\n * failure mode this check exists to avoid.\n */\nexport function resolvePermissionModeForStartup(options: {\n\tinteractive: boolean;\n\trequestedMode: PermissionMode | undefined;\n}): PermissionModeStartupResult {\n\tif (options.requestedMode !== undefined) {\n\t\treturn { ok: true, mode: options.requestedMode };\n\t}\n\tif (!options.interactive) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Non-interactive sessions require an explicit --permission-mode. Valid modes: ${PERMISSION_MODES.join(\", \")}.`,\n\t\t};\n\t}\n\treturn { ok: true, mode: \"default\" };\n}\n\n/** Sources a mode can come from, in precedence order — mirrors PERMISSION_SOURCES (rules.ts), restricted to the sources that can plausibly set one. */\nconst MODE_SOURCE_ORDER: readonly [\"flag\", ...WritablePermissionSource[]] = [\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"command\",\n\t\"session\",\n];\n\n/**\n * The `--permission-mode` flag outranks every persisted mode, including a project\n * rule a team committed — a deliberate per-run operator decision should not be\n * silently overridden by checked-in config (ADR 0004).\n */\nexport function resolveEffectiveMode(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): PermissionMode {\n\tfor (const source of MODE_SOURCE_ORDER) {\n\t\tif (source === \"flag\") {\n\t\t\tif (flagMode !== undefined) return flagMode;\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode = modesBySource.get(source);\n\t\tif (mode !== undefined) return mode;\n\t}\n\treturn \"default\";\n}\n"]}
1
+ {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../../../src/core/permissions/startup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAoB,KAAK,cAAc,EAAE,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAElG,MAAM,MAAM,2BAA2B,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9G;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACxD,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,cAAc,GAAG,SAAS,CAAC;CAC1C,GAAG,2BAA2B,CAW9B;AAYD,4EAA4E;AAC5E,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,wBAAwB,GAAG,SAAS,CAAC;AAEjF,MAAM,WAAW,uBAAuB;IACvC,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,oBAAoB,CAAC;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAC7C,QAAQ,EAAE,cAAc,GAAG,SAAS,EACpC,aAAa,EAAE,WAAW,CAAC,wBAAwB,EAAE,cAAc,CAAC,GAClE,uBAAuB,CAUzB;AAED,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,cAAc,GAAG,SAAS,EACpC,aAAa,EAAE,WAAW,CAAC,wBAAwB,EAAE,cAAc,CAAC,GAClE,cAAc,CAEhB","sourcesContent":["/**\n * Startup-time permission mode resolution (ADR 0004). A misconfigured\n * non-interactive run should fail fast, at startup, with an actionable message —\n * not deny every tool call mid-run in a way that looks like a bug (see the ADR's\n * \"ask fails closed, and a session that cannot ask refuses to start\").\n */\n\nimport { PERMISSION_MODES, type PermissionMode, type WritablePermissionSource } from \"./store.ts\";\n\nexport type PermissionModeStartupResult = { ok: true; mode: PermissionMode } | { ok: false; message: string };\n\n/**\n * `requestedMode` is assumed already validated against `PERMISSION_MODES` (args.ts\n * rejects an unknown value at parse time) — this only decides whether a session may\n * start *without* one. Interactive sessions may: an `ask` there has a human to\n * resolve it. Non-interactive sessions may not: with no responder, `ask` fails\n * closed to `deny`, and discovering that per-call, mid-run, is the exact bad\n * failure mode this check exists to avoid.\n */\nexport function resolvePermissionModeForStartup(options: {\n\tinteractive: boolean;\n\trequestedMode: PermissionMode | undefined;\n}): PermissionModeStartupResult {\n\tif (options.requestedMode !== undefined) {\n\t\treturn { ok: true, mode: options.requestedMode };\n\t}\n\tif (!options.interactive) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Non-interactive sessions require an explicit --permission-mode. Valid modes: ${PERMISSION_MODES.join(\", \")}.`,\n\t\t};\n\t}\n\treturn { ok: true, mode: \"default\" };\n}\n\n/** Sources a mode can come from, in precedence order — mirrors PERMISSION_SOURCES (rules.ts), restricted to the sources that can plausibly set one. */\nconst MODE_SOURCE_ORDER: readonly [\"flag\", ...WritablePermissionSource[]] = [\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"command\",\n\t\"session\",\n];\n\n/** Where an effective mode came from. `\"default\"` means nothing set one. */\nexport type PermissionModeOrigin = \"flag\" | WritablePermissionSource | \"default\";\n\nexport interface EffectiveModeResolution {\n\tmode: PermissionMode;\n\torigin: PermissionModeOrigin;\n}\n\n/**\n * The `--permission-mode` flag outranks every persisted mode, including a project\n * rule a team committed — a deliberate per-run operator decision should not be\n * silently overridden by checked-in config (ADR 0004).\n *\n * Returns the origin alongside the mode so a caller that offers to *change* the\n * mode can tell whether its write would actually take effect. A settings toggle\n * writing `user` while `local` or the flag outranks it is a silent no-op, which is\n * the one failure a UI here must not have.\n */\nexport function resolveEffectiveModeWithOrigin(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): EffectiveModeResolution {\n\tfor (const source of MODE_SOURCE_ORDER) {\n\t\tif (source === \"flag\") {\n\t\t\tif (flagMode !== undefined) return { mode: flagMode, origin: \"flag\" };\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode = modesBySource.get(source);\n\t\tif (mode !== undefined) return { mode, origin: source };\n\t}\n\treturn { mode: \"default\", origin: \"default\" };\n}\n\nexport function resolveEffectiveMode(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): PermissionMode {\n\treturn resolveEffectiveModeWithOrigin(flagMode, modesBySource).mode;\n}\n"]}
@@ -38,18 +38,26 @@ const MODE_SOURCE_ORDER = [
38
38
  * The `--permission-mode` flag outranks every persisted mode, including a project
39
39
  * rule a team committed — a deliberate per-run operator decision should not be
40
40
  * silently overridden by checked-in config (ADR 0004).
41
+ *
42
+ * Returns the origin alongside the mode so a caller that offers to *change* the
43
+ * mode can tell whether its write would actually take effect. A settings toggle
44
+ * writing `user` while `local` or the flag outranks it is a silent no-op, which is
45
+ * the one failure a UI here must not have.
41
46
  */
42
- export function resolveEffectiveMode(flagMode, modesBySource) {
47
+ export function resolveEffectiveModeWithOrigin(flagMode, modesBySource) {
43
48
  for (const source of MODE_SOURCE_ORDER) {
44
49
  if (source === "flag") {
45
50
  if (flagMode !== undefined)
46
- return flagMode;
51
+ return { mode: flagMode, origin: "flag" };
47
52
  continue;
48
53
  }
49
54
  const mode = modesBySource.get(source);
50
55
  if (mode !== undefined)
51
- return mode;
56
+ return { mode, origin: source };
52
57
  }
53
- return "default";
58
+ return { mode: "default", origin: "default" };
59
+ }
60
+ export function resolveEffectiveMode(flagMode, modesBySource) {
61
+ return resolveEffectiveModeWithOrigin(flagMode, modesBySource).mode;
54
62
  }
55
63
  //# sourceMappingURL=startup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"startup.js","sourceRoot":"","sources":["../../../src/core/permissions/startup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAsD,MAAM,YAAY,CAAC;AAIlG;;;;;;;GAOG;AACH,MAAM,UAAU,+BAA+B,CAAC,OAG/C,EAA+B;IAC/B,IAAI,OAAO,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QACzC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC;IAClD,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAC1B,OAAO;YACN,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,gFAAgF,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SACvH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAAA,CACrC;AAED,yJAAuJ;AACvJ,MAAM,iBAAiB,GAAqD;IAC3E,MAAM;IACN,OAAO;IACP,SAAS;IACT,MAAM;IACN,SAAS;IACT,SAAS;CACT,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CACnC,QAAoC,EACpC,aAAoE,EACnD;IACjB,KAAK,MAAM,MAAM,IAAI,iBAAiB,EAAE,CAAC;QACxC,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACvB,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,QAAQ,CAAC;YAC5C,SAAS;QACV,CAAC;QACD,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;IACrC,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB","sourcesContent":["/**\n * Startup-time permission mode resolution (ADR 0004). A misconfigured\n * non-interactive run should fail fast, at startup, with an actionable message —\n * not deny every tool call mid-run in a way that looks like a bug (see the ADR's\n * \"ask fails closed, and a session that cannot ask refuses to start\").\n */\n\nimport { PERMISSION_MODES, type PermissionMode, type WritablePermissionSource } from \"./store.ts\";\n\nexport type PermissionModeStartupResult = { ok: true; mode: PermissionMode } | { ok: false; message: string };\n\n/**\n * `requestedMode` is assumed already validated against `PERMISSION_MODES` (args.ts\n * rejects an unknown value at parse time) — this only decides whether a session may\n * start *without* one. Interactive sessions may: an `ask` there has a human to\n * resolve it. Non-interactive sessions may not: with no responder, `ask` fails\n * closed to `deny`, and discovering that per-call, mid-run, is the exact bad\n * failure mode this check exists to avoid.\n */\nexport function resolvePermissionModeForStartup(options: {\n\tinteractive: boolean;\n\trequestedMode: PermissionMode | undefined;\n}): PermissionModeStartupResult {\n\tif (options.requestedMode !== undefined) {\n\t\treturn { ok: true, mode: options.requestedMode };\n\t}\n\tif (!options.interactive) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Non-interactive sessions require an explicit --permission-mode. Valid modes: ${PERMISSION_MODES.join(\", \")}.`,\n\t\t};\n\t}\n\treturn { ok: true, mode: \"default\" };\n}\n\n/** Sources a mode can come from, in precedence order — mirrors PERMISSION_SOURCES (rules.ts), restricted to the sources that can plausibly set one. */\nconst MODE_SOURCE_ORDER: readonly [\"flag\", ...WritablePermissionSource[]] = [\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"command\",\n\t\"session\",\n];\n\n/**\n * The `--permission-mode` flag outranks every persisted mode, including a project\n * rule a team committed — a deliberate per-run operator decision should not be\n * silently overridden by checked-in config (ADR 0004).\n */\nexport function resolveEffectiveMode(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): PermissionMode {\n\tfor (const source of MODE_SOURCE_ORDER) {\n\t\tif (source === \"flag\") {\n\t\t\tif (flagMode !== undefined) return flagMode;\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode = modesBySource.get(source);\n\t\tif (mode !== undefined) return mode;\n\t}\n\treturn \"default\";\n}\n"]}
1
+ {"version":3,"file":"startup.js","sourceRoot":"","sources":["../../../src/core/permissions/startup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAsD,MAAM,YAAY,CAAC;AAIlG;;;;;;;GAOG;AACH,MAAM,UAAU,+BAA+B,CAAC,OAG/C,EAA+B;IAC/B,IAAI,OAAO,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QACzC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC;IAClD,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAC1B,OAAO;YACN,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,gFAAgF,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SACvH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAAA,CACrC;AAED,yJAAuJ;AACvJ,MAAM,iBAAiB,GAAqD;IAC3E,MAAM;IACN,OAAO;IACP,SAAS;IACT,MAAM;IACN,SAAS;IACT,SAAS;CACT,CAAC;AAUF;;;;;;;;;GASG;AACH,MAAM,UAAU,8BAA8B,CAC7C,QAAoC,EACpC,aAAoE,EAC1C;IAC1B,KAAK,MAAM,MAAM,IAAI,iBAAiB,EAAE,CAAC;QACxC,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACvB,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;YACtE,SAAS;QACV,CAAC;QACD,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACzD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAAA,CAC9C;AAED,MAAM,UAAU,oBAAoB,CACnC,QAAoC,EACpC,aAAoE,EACnD;IACjB,OAAO,8BAA8B,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,IAAI,CAAC;AAAA,CACpE","sourcesContent":["/**\n * Startup-time permission mode resolution (ADR 0004). A misconfigured\n * non-interactive run should fail fast, at startup, with an actionable message —\n * not deny every tool call mid-run in a way that looks like a bug (see the ADR's\n * \"ask fails closed, and a session that cannot ask refuses to start\").\n */\n\nimport { PERMISSION_MODES, type PermissionMode, type WritablePermissionSource } from \"./store.ts\";\n\nexport type PermissionModeStartupResult = { ok: true; mode: PermissionMode } | { ok: false; message: string };\n\n/**\n * `requestedMode` is assumed already validated against `PERMISSION_MODES` (args.ts\n * rejects an unknown value at parse time) — this only decides whether a session may\n * start *without* one. Interactive sessions may: an `ask` there has a human to\n * resolve it. Non-interactive sessions may not: with no responder, `ask` fails\n * closed to `deny`, and discovering that per-call, mid-run, is the exact bad\n * failure mode this check exists to avoid.\n */\nexport function resolvePermissionModeForStartup(options: {\n\tinteractive: boolean;\n\trequestedMode: PermissionMode | undefined;\n}): PermissionModeStartupResult {\n\tif (options.requestedMode !== undefined) {\n\t\treturn { ok: true, mode: options.requestedMode };\n\t}\n\tif (!options.interactive) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `Non-interactive sessions require an explicit --permission-mode. Valid modes: ${PERMISSION_MODES.join(\", \")}.`,\n\t\t};\n\t}\n\treturn { ok: true, mode: \"default\" };\n}\n\n/** Sources a mode can come from, in precedence order — mirrors PERMISSION_SOURCES (rules.ts), restricted to the sources that can plausibly set one. */\nconst MODE_SOURCE_ORDER: readonly [\"flag\", ...WritablePermissionSource[]] = [\n\t\"flag\",\n\t\"local\",\n\t\"project\",\n\t\"user\",\n\t\"command\",\n\t\"session\",\n];\n\n/** Where an effective mode came from. `\"default\"` means nothing set one. */\nexport type PermissionModeOrigin = \"flag\" | WritablePermissionSource | \"default\";\n\nexport interface EffectiveModeResolution {\n\tmode: PermissionMode;\n\torigin: PermissionModeOrigin;\n}\n\n/**\n * The `--permission-mode` flag outranks every persisted mode, including a project\n * rule a team committed — a deliberate per-run operator decision should not be\n * silently overridden by checked-in config (ADR 0004).\n *\n * Returns the origin alongside the mode so a caller that offers to *change* the\n * mode can tell whether its write would actually take effect. A settings toggle\n * writing `user` while `local` or the flag outranks it is a silent no-op, which is\n * the one failure a UI here must not have.\n */\nexport function resolveEffectiveModeWithOrigin(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): EffectiveModeResolution {\n\tfor (const source of MODE_SOURCE_ORDER) {\n\t\tif (source === \"flag\") {\n\t\t\tif (flagMode !== undefined) return { mode: flagMode, origin: \"flag\" };\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode = modesBySource.get(source);\n\t\tif (mode !== undefined) return { mode, origin: source };\n\t}\n\treturn { mode: \"default\", origin: \"default\" };\n}\n\nexport function resolveEffectiveMode(\n\tflagMode: PermissionMode | undefined,\n\tmodesBySource: ReadonlyMap<WritablePermissionSource, PermissionMode>,\n): PermissionMode {\n\treturn resolveEffectiveModeWithOrigin(flagMode, modesBySource).mode;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyCH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AA0gBlE,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,eAAe,EAAE,CAAC;CACvC;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBA4b/D","sourcesContent":["/**\n * Main entry point for the coding agent CLI.\n *\n * This file handles CLI argument parsing and translates them into\n * createAgentSession() options. The SDK does the heavy lifting.\n */\n\nimport { createInterface } from \"node:readline\";\nimport { type ImageContent, modelsAreEqual } from \"@earendil-works/pi-ai\";\nimport chalk from \"chalk\";\nimport { type Args, type Mode, parseArgs, printHelp } from \"./cli/args.ts\";\nimport {\n\ttype AuthCheckResult,\n\tcheckProviderAuth,\n\tcreateAuthCheckModelRuntime,\n\tgetProviderCredential,\n} from \"./cli/auth-check.ts\";\nimport {\n\ttype AuthCommand,\n\tAuthCommandError,\n\tgetAuthCommandName,\n\tgetAuthCommandUsage,\n\tisAuthCommandHelp,\n\tparseAuthCommand,\n\tprintAuthCommandHelp,\n\tvalidateAuthCommandArgs,\n} from \"./cli/auth-command.ts\";\nimport { runCostCommand } from \"./cli/cost-command.ts\";\nimport { resolveCredentialForPrint } from \"./cli/credential-print.ts\";\nimport { processFileArguments } from \"./cli/file-processor.ts\";\nimport { buildInitialMessage } from \"./cli/initial-message.ts\";\nimport { listModels } from \"./cli/list-models.ts\";\nimport { createProjectTrustContext } from \"./cli/project-trust.ts\";\nimport { selectSession } from \"./cli/session-picker.ts\";\nimport { shouldRunFirstTimeSetup, showFirstTimeSetup, showStartupSelector } from \"./cli/startup-ui.ts\";\nimport { APP_NAME, ENV_SESSION_DIR, expandTildePath, getAgentDir, getPackageDir, VERSION } from \"./config.ts\";\nimport { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from \"./core/agent-session-runtime.ts\";\nimport {\n\ttype AgentSessionRuntimeDiagnostic,\n\tcreateAgentSessionFromServices,\n\tcreateAgentSessionServices,\n} from \"./core/agent-session-services.ts\";\nimport { formatNoModelsAvailableMessage } from \"./core/auth-guidance.ts\";\nimport { AuthStorage, ReadOnlyAuthStorage } from \"./core/auth-storage.ts\";\nimport { getApexEnvironment, setApexEnvironment } from \"./core/environment.ts\";\nimport { exportFromFile } from \"./core/export-html/index.ts\";\nimport type { InlineExtension } from \"./core/extensions/types.ts\";\nimport { applyHttpProxySettings, configureHttpDispatcher } from \"./core/http-dispatcher.ts\";\nimport { resolveCliModel, resolveModelScope, type ScopedModel } from \"./core/model-resolver.ts\";\nimport { ModelRuntime } from \"./core/model-runtime.ts\";\nimport { restoreStdout, takeOverStdout } from \"./core/output-guard.ts\";\nimport { resolveEffectiveMode, resolvePermissionModeForStartup } from \"./core/permissions/startup.ts\";\nimport { FilePermissionRuleStore } from \"./core/permissions/store.ts\";\nimport { type AppMode, resolveProjectTrusted } from \"./core/project-trust.ts\";\nimport type { CreateAgentSessionOptions } from \"./core/sdk.ts\";\nimport {\n\tformatMissingSessionCwdPrompt,\n\tgetMissingSessionCwdIssue,\n\tMissingSessionCwdError,\n\ttype SessionCwdIssue,\n} from \"./core/session-cwd.ts\";\nimport { assertValidSessionId, SessionManager } from \"./core/session-manager.ts\";\nimport { SettingsManager } from \"./core/settings-manager.ts\";\nimport { printTimings, resetTimings, time } from \"./core/timings.ts\";\nimport { hasTrustRequiringProjectResources, ProjectTrustStore } from \"./core/trust-manager.ts\";\nimport { builtInExtensions } from \"./extensions/index.ts\";\nimport { runMigrations, showDeprecationWarnings } from \"./migrations.ts\";\nimport { InteractiveMode, runPrintMode, runRpcMode } from \"./modes/index.ts\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.ts\";\nimport { handleConfigCommand, handlePackageCommand } from \"./package-manager-cli.ts\";\nimport { isLocalPath, normalizePath, resolvePath } from \"./utils/paths.ts\";\nimport { cleanupWindowsSelfUpdateQuarantine } from \"./utils/windows-self-update.ts\";\n\nconst EXTENSION_LOAD_FAILURE_HINT = 'Hint: Start without extensions using \"apex-code -ne\".';\n\n/**\n * Read all content from piped stdin.\n * Returns undefined if stdin is a TTY (interactive terminal).\n */\nasync function readPipedStdin(): Promise<string | undefined> {\n\t// If stdin is a TTY, we're running interactively - don't read stdin\n\tif (process.stdin.isTTY) {\n\t\treturn undefined;\n\t}\n\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tprocess.stdin.setEncoding(\"utf8\");\n\t\tprocess.stdin.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tprocess.stdin.on(\"end\", () => {\n\t\t\tresolve(data.trim() || undefined);\n\t\t});\n\t\tprocess.stdin.resume();\n\t});\n}\n\nfunction collectSettingsDiagnostics(\n\tsettingsManager: SettingsManager,\n\tcontext: string,\n): AgentSessionRuntimeDiagnostic[] {\n\treturn settingsManager.drainErrors().map(({ scope, error }) => ({\n\t\ttype: \"warning\",\n\t\tmessage: `(${context}, ${scope} settings) ${error.message}`,\n\t}));\n}\n\nfunction reportDiagnostics(diagnostics: readonly AgentSessionRuntimeDiagnostic[]): void {\n\tfor (const diagnostic of diagnostics) {\n\t\tconst color = diagnostic.type === \"error\" ? chalk.red : diagnostic.type === \"warning\" ? chalk.yellow : chalk.dim;\n\t\tconst prefix = diagnostic.type === \"error\" ? \"Error: \" : diagnostic.type === \"warning\" ? \"Warning: \" : \"\";\n\t\tconsole.error(color(`${prefix}${diagnostic.message}`));\n\t}\n}\n\nfunction isTruthyEnvFlag(value: string | undefined): boolean {\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\nfunction resolveAppMode(parsed: Args, stdinIsTTY: boolean, stdoutIsTTY: boolean): AppMode {\n\tif (parsed.mode === \"rpc\") {\n\t\treturn \"rpc\";\n\t}\n\tif (parsed.mode === \"json\") {\n\t\treturn \"json\";\n\t}\n\tif (parsed.print || !stdinIsTTY || !stdoutIsTTY) {\n\t\treturn \"print\";\n\t}\n\treturn \"interactive\";\n}\n\nfunction toPrintOutputMode(appMode: AppMode): Exclude<Mode, \"rpc\"> {\n\treturn appMode === \"json\" ? \"json\" : \"text\";\n}\n\nfunction isPlainRuntimeMetadataCommand(parsed: Args): boolean {\n\treturn !parsed.print && parsed.mode === undefined && (parsed.help === true || parsed.listModels !== undefined);\n}\n\nasync function runAuthCommand(args: string[]): Promise<boolean> {\n\tif (isAuthCommandHelp(args)) {\n\t\tprintAuthCommandHelp();\n\t\treturn true;\n\t}\n\n\tlet command: AuthCommand | undefined;\n\ttry {\n\t\tcommand = parseAuthCommand(args);\n\t} catch (error) {\n\t\tconst message = error instanceof AuthCommandError ? error.message : \"Failed to parse auth command\";\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exitCode = 1;\n\t\treturn true;\n\t}\n\tif (!command) return false;\n\n\tconst parsed = parseArgs(command.args);\n\tif (parsed.unknownFlags.size > 0) {\n\t\tconst option = parsed.unknownFlags.keys().next().value;\n\t\tconsole.error(chalk.red(`Unknown option --${option} for \"${getAuthCommandName(command.kind)}\".`));\n\t\tconsole.error(chalk.dim(`Use \"${APP_NAME} --help\" or \"${getAuthCommandUsage(command.kind)}\".`));\n\t\tprocess.exitCode = 1;\n\t\treturn true;\n\t}\n\ttry {\n\t\tif (parsed.diagnostics.length > 0) {\n\t\t\tthrow new AuthCommandError(parsed.diagnostics.map((diagnostic) => diagnostic.message).join(\"\\n\"));\n\t\t}\n\t\tif (command.kind !== \"check\") {\n\t\t\tconst signal = AbortSignal.timeout(15_000);\n\t\t\tconst modelRuntime = await ModelRuntime.create({ allowModelNetwork: false, signal });\n\t\t\tconst credential = await resolveCredentialForPrint(\n\t\t\t\tparsed,\n\t\t\t\tmodelRuntime,\n\t\t\t\tcommand.kind,\n\t\t\t\tcommand.minExpiryMs,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tprocess.stdout.write(`${credential}\\n`);\n\t\t\treturn true;\n\t\t}\n\n\t\tconst requestedAuth = validateAuthCommandArgs(parsed, command.kind);\n\t\tlet result: AuthCheckResult;\n\t\tlet credential: string | undefined;\n\t\ttry {\n\t\t\tconst credentials = command.noRefresh ? new ReadOnlyAuthStorage() : AuthStorage.create();\n\t\t\tconst modelRuntime = await createAuthCheckModelRuntime(credentials);\n\t\t\tresult = await checkProviderAuth(parsed, modelRuntime, { refresh: !command.noRefresh });\n\t\t\tif (command.credentials && result.status === \"ready\") {\n\t\t\t\tcredential = await getProviderCredential(result.provider, modelRuntime, credentials, {\n\t\t\t\t\trefresh: !command.noRefresh,\n\t\t\t\t});\n\t\t\t\tif (!credential) {\n\t\t\t\t\tresult = { status: \"not_ready\", provider: result.provider, reason: \"credential_not_available\" };\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\tresult = {\n\t\t\t\tstatus: \"invalid\",\n\t\t\t\tprovider: requestedAuth.provider ?? requestedAuth.model!,\n\t\t\t\treason: \"invalid_state\",\n\t\t\t};\n\t\t}\n\t\tconst output = command.json\n\t\t\t? JSON.stringify({ ...result, ...(credential ? { credentials: credential } : {}) })\n\t\t\t: (credential ?? result.status);\n\t\tprocess.stdout.write(`${output}\\n`);\n\t\tprocess.exitCode = result.status === \"ready\" ? 0 : result.status === \"not_ready\" ? 1 : 2;\n\t} catch (error) {\n\t\tconst message = error instanceof AuthCommandError ? error.message : \"Failed to resolve credential\";\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exitCode = command.kind === \"check\" ? 2 : 1;\n\t}\n\treturn true;\n}\n\nasync function prepareInitialMessage(\n\tparsed: Args,\n\tautoResizeImages: boolean,\n\tstdinContent?: string,\n): Promise<{\n\tinitialMessage?: string;\n\tinitialImages?: ImageContent[];\n}> {\n\tif (parsed.fileArgs.length === 0) {\n\t\treturn buildInitialMessage({ parsed, stdinContent });\n\t}\n\n\tconst { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });\n\treturn buildInitialMessage({\n\t\tparsed,\n\t\tfileText: text,\n\t\tfileImages: images,\n\t\tstdinContent,\n\t});\n}\n\n/** Result from resolving a session argument */\ntype ResolvedSession =\n\t| { type: \"path\"; path: string } // Direct file path\n\t| { type: \"local\"; path: string } // Found in current project\n\t| { type: \"global\"; path: string; cwd: string } // Found in different project\n\t| { type: \"not_found\"; arg: string }; // Not found anywhere\n\n/**\n * Resolve a session argument to a file path.\n * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.\n */\nasync function findLocalSessionByExactId(\n\tsessionId: string,\n\tcwd: string,\n\tsessionDir?: string,\n): Promise<{ type: \"local\"; path: string } | undefined> {\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatch = localSessions.find((s) => s.id === sessionId);\n\treturn localMatch ? { type: \"local\", path: localMatch.path } : undefined;\n}\n\nasync function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise<ResolvedSession> {\n\t// If it looks like a file path, resolve it before handing it to the session manager.\n\tif (sessionArg.includes(\"/\") || sessionArg.includes(\"\\\\\") || sessionArg.endsWith(\".jsonl\")) {\n\t\treturn { type: \"path\", path: resolvePath(sessionArg, cwd) };\n\t}\n\n\t// Try to match as session ID in current project first\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatch =\n\t\tlocalSessions.find((s) => s.id === sessionArg) ?? localSessions.find((s) => s.id.startsWith(sessionArg));\n\n\tif (localMatch) {\n\t\treturn { type: \"local\", path: localMatch.path };\n\t}\n\n\t// Try global search across all projects\n\tconst allSessions = await SessionManager.listAll(sessionDir);\n\tconst globalMatch =\n\t\tallSessions.find((s) => s.id === sessionArg) ?? allSessions.find((s) => s.id.startsWith(sessionArg));\n\n\tif (globalMatch) {\n\t\treturn { type: \"global\", path: globalMatch.path, cwd: globalMatch.cwd };\n\t}\n\n\t// Not found anywhere\n\treturn { type: \"not_found\", arg: sessionArg };\n}\n\n/** Prompt user for yes/no confirmation */\nasync function promptConfirm(message: string): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst rl = createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\t\trl.question(`${message} [y/N] `, (answer) => {\n\t\t\trl.close();\n\t\t\tresolve(answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\");\n\t\t});\n\t});\n}\n\nfunction validateForkFlags(parsed: Args): void {\n\tif (!parsed.fork) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t\tparsed.noSession ? \"--no-session\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction validateSessionIdFlags(parsed: Args): void {\n\tif (parsed.sessionId === undefined) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --session-id cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n\n\ttry {\n\t\tassertValidSessionId(parsed.sessionId);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction openSessionOrExit(path: string, sessionDir?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.open(path, sessionDir);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction forkSessionOrExit(sourcePath: string, cwd: string, sessionDir?: string, sessionId?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.forkFrom(sourcePath, cwd, sessionDir, { id: sessionId });\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nasync function createSessionManager(\n\tparsed: Args,\n\tcwd: string,\n\tsessionDir: string | undefined,\n\tsettingsManager: SettingsManager,\n): Promise<SessionManager> {\n\tif (parsed.noSession || parsed.help || parsed.listModels !== undefined) {\n\t\treturn SessionManager.inMemory(cwd, parsed.sessionId !== undefined ? { id: parsed.sessionId } : undefined);\n\t}\n\n\tif (parsed.fork) {\n\t\tif (parsed.sessionId) {\n\t\t\tconst existingTarget = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);\n\t\t\tif (existingTarget) {\n\t\t\t\tconsole.error(chalk.red(`Session already exists with id '${parsed.sessionId}'`));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\n\t\tconst resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\tcase \"global\":\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir, parsed.sessionId);\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.session) {\n\t\tconst resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\t\treturn openSessionOrExit(resolved.path, sessionDir);\n\n\t\t\tcase \"global\": {\n\t\t\t\tconsole.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));\n\t\t\t\tconst shouldFork = await promptConfirm(\"Fork this session into current directory?\");\n\t\t\t\tif (!shouldFork) {\n\t\t\t\t\tconsole.log(chalk.dim(\"Aborted.\"));\n\t\t\t\t\tprocess.exit(0);\n\t\t\t\t}\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\t\t\t}\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.resume) {\n\t\ttry {\n\t\t\tconst selectedPath = await selectSession(\n\t\t\t\t(onProgress) => SessionManager.list(cwd, sessionDir, onProgress),\n\t\t\t\t(onProgress) => SessionManager.listAll(sessionDir, onProgress),\n\t\t\t\tsettingsManager,\n\t\t\t);\n\t\t\tif (!selectedPath) {\n\t\t\t\tconsole.log(chalk.dim(\"No session selected\"));\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\treturn SessionManager.open(selectedPath, sessionDir);\n\t\t} finally {\n\t\t\tstopThemeWatcher();\n\t\t}\n\t}\n\n\tif (parsed.continue) {\n\t\treturn SessionManager.continueRecent(cwd, sessionDir);\n\t}\n\n\tif (parsed.sessionId) {\n\t\tconst existingSession = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);\n\t\tif (existingSession) {\n\t\t\treturn SessionManager.open(existingSession.path, sessionDir);\n\t\t}\n\t\tconsole.error(\n\t\t\tchalk.yellow(\n\t\t\t\t`Warning: No project session found with id '${parsed.sessionId}'; creating a new session with that id.`,\n\t\t\t),\n\t\t);\n\t}\n\n\treturn SessionManager.create(cwd, sessionDir, { id: parsed.sessionId });\n}\n\nfunction buildSessionOptions(\n\tparsed: Args,\n\tscopedModels: ScopedModel[],\n\thasExistingSession: boolean,\n\tmodelRuntime: ModelRuntime,\n\tsettingsManager: SettingsManager,\n): {\n\toptions: CreateAgentSessionOptions;\n\tcliThinkingFromModel: boolean;\n\tdiagnostics: AgentSessionRuntimeDiagnostic[];\n} {\n\tconst options: CreateAgentSessionOptions = {};\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tlet cliThinkingFromModel = false;\n\n\t// Model from CLI\n\t// - supports --provider <name> --model <pattern>\n\t// - supports --model <provider>/<pattern>\n\tif (parsed.model) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider: parsed.provider,\n\t\t\tcliModel: parsed.model,\n\t\t\tcliThinking: parsed.thinking,\n\t\t\tmodelRuntime,\n\t\t});\n\t\tif (resolved.warning) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: resolved.warning });\n\t\t}\n\t\tif (resolved.error) {\n\t\t\tdiagnostics.push({ type: \"error\", message: resolved.error });\n\t\t}\n\t\tif (resolved.model) {\n\t\t\toptions.model = resolved.model;\n\t\t\t// Allow \"--model <pattern>:<thinking>\" as a shorthand.\n\t\t\t// Explicit --thinking still takes precedence (applied later).\n\t\t\tif (!parsed.thinking && resolved.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = resolved.thinkingLevel;\n\t\t\t\tcliThinkingFromModel = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!options.model && scopedModels.length > 0 && !hasExistingSession) {\n\t\t// Check if saved default is in scoped models - use it if so, otherwise first scoped model\n\t\tconst savedProvider = settingsManager.getDefaultProvider();\n\t\tconst savedModelId = settingsManager.getDefaultModel();\n\t\tconst savedModel = savedProvider && savedModelId ? modelRuntime.getModel(savedProvider, savedModelId) : undefined;\n\t\tconst savedInScope = savedModel ? scopedModels.find((sm) => modelsAreEqual(sm.model, savedModel)) : undefined;\n\n\t\tif (savedInScope) {\n\t\t\toptions.model = savedInScope.model;\n\t\t\t// Use thinking level from scoped model config if explicitly set\n\t\t\tif (!parsed.thinking && savedInScope.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = savedInScope.thinkingLevel;\n\t\t\t}\n\t\t} else {\n\t\t\toptions.model = scopedModels[0].model;\n\t\t\t// Use thinking level from first scoped model if explicitly set\n\t\t\tif (!parsed.thinking && scopedModels[0].thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = scopedModels[0].thinkingLevel;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Thinking level from CLI (takes precedence over scoped model thinking levels set above)\n\tif (parsed.thinking) {\n\t\toptions.thinkingLevel = parsed.thinking;\n\t}\n\n\t// Scoped models for Ctrl+P cycling\n\t// Keep thinking level undefined when not explicitly set in the model pattern.\n\t// Undefined means \"inherit current session thinking level\" during cycling.\n\tif (scopedModels.length > 0) {\n\t\toptions.scopedModels = scopedModels.map((sm) => ({\n\t\t\tmodel: sm.model,\n\t\t\tthinkingLevel: sm.thinkingLevel,\n\t\t}));\n\t}\n\n\t// API key from CLI - set as a non-persistent runtime override\n\t// (handled by caller before createAgentSession)\n\n\t// Tools\n\tif (parsed.noTools) {\n\t\toptions.noTools = \"all\";\n\t} else if (parsed.noBuiltinTools) {\n\t\toptions.noTools = \"builtin\";\n\t}\n\tif (parsed.tools) {\n\t\toptions.tools = [...parsed.tools];\n\t}\n\tif (parsed.excludeTools) {\n\t\toptions.excludeTools = [...parsed.excludeTools];\n\t}\n\n\treturn { options, cliThinkingFromModel, diagnostics };\n}\n\nfunction resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined {\n\treturn paths?.map((value) => (isLocalPath(value) ? resolvePath(value, cwd) : value));\n}\n\nasync function promptForMissingSessionCwd(\n\tissue: SessionCwdIssue,\n\tsettingsManager: SettingsManager,\n): Promise<string | undefined> {\n\treturn showStartupSelector(settingsManager, formatMissingSessionCwdPrompt(issue), [\n\t\t{ label: \"Continue\", value: issue.fallbackCwd },\n\t\t{ label: \"Cancel\", value: undefined },\n\t]);\n}\n\nexport interface MainOptions {\n\textensionFactories?: InlineExtension[];\n}\n\nexport async function main(args: string[], options?: MainOptions) {\n\tresetTimings();\n\tconst extensionFactories = [...builtInExtensions, ...(options?.extensionFactories ?? [])];\n\tconst offlineMode = args.includes(\"--offline\") || isTruthyEnvFlag(getApexEnvironment(\"APEX_CODE_OFFLINE\"));\n\tif (offlineMode) {\n\t\tsetApexEnvironment(\"APEX_CODE_OFFLINE\", \"1\");\n\t\tsetApexEnvironment(\"APEX_CODE_SKIP_VERSION_CHECK\", \"1\");\n\t}\n\n\tif (await runAuthCommand(args)) {\n\t\treturn;\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\tcleanupWindowsSelfUpdateQuarantine(getPackageDir());\n\t}\n\n\tconst cwd = process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst bootstrapSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tapplyHttpProxySettings(bootstrapSettingsManager.getGlobalSettings().httpProxy);\n\tconfigureHttpDispatcher();\n\n\tif (args[0] === \"cost\") {\n\t\tawait runCostCommand(agentDir, args.slice(1));\n\t\treturn;\n\t}\n\n\tif (await handlePackageCommand(args, { extensionFactories })) {\n\t\tconst exitCode = process.exitCode ?? 0;\n\t\tif (process.platform === \"win32\" && exitCode === 0 && args[0] === \"update\") {\n\t\t\t// We normally prefer process.exit(0) for package commands so bad extensions cannot keep\n\t\t\t// one-shot commands alive. On Windows, Node can assert after fetch() if process.exit(0)\n\t\t\t// runs during teardown; let successful `pi update` drain naturally instead.\n\t\t\t// https://github.com/nodejs/node/issues/56645\n\t\t\treturn;\n\t\t}\n\t\tprocess.exit(exitCode);\n\t\treturn;\n\t}\n\n\tif (await handleConfigCommand(args, { extensionFactories })) {\n\t\treturn;\n\t}\n\n\tconst parsed = parseArgs(args);\n\tif (parsed.diagnostics.length > 0) {\n\t\tfor (const d of parsed.diagnostics) {\n\t\t\tconst color = d.type === \"error\" ? chalk.red : chalk.yellow;\n\t\t\tconsole.error(color(`${d.type === \"error\" ? \"Error\" : \"Warning\"}: ${d.message}`));\n\t\t}\n\t\tif (parsed.diagnostics.some((d) => d.type === \"error\")) {\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"parseArgs\");\n\n\tif (parsed.version) {\n\t\tconsole.log(VERSION);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.export) {\n\t\tlet result: string;\n\t\ttry {\n\t\t\tconst outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;\n\t\t\tresult = await exportFromFile(parsed.export, outputPath);\n\t\t} catch (error: unknown) {\n\t\t\tconst message = error instanceof Error ? error.message : \"Failed to export session\";\n\t\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconsole.log(`Exported to: ${result}`);\n\t\tprocess.exit(0);\n\t}\n\n\tlet appMode = resolveAppMode(parsed, process.stdin.isTTY, process.stdout.isTTY);\n\tconst shouldTakeOverStdout = appMode !== \"interactive\" && !isPlainRuntimeMetadataCommand(parsed);\n\tif (shouldTakeOverStdout) {\n\t\ttakeOverStdout();\n\t}\n\n\t// A headless caller cannot answer an `ask`; require an explicit operator\n\t// decision before creating a session. Metadata commands never create one.\n\tconst permissionModeStartup = resolvePermissionModeForStartup({\n\t\tinteractive: appMode === \"interactive\",\n\t\trequestedMode: parsed.permissionMode,\n\t});\n\tif (!permissionModeStartup.ok && !parsed.help && parsed.listModels === undefined) {\n\t\tconsole.error(chalk.red(`Error: ${permissionModeStartup.message}`));\n\t\tprocess.exit(1);\n\t}\n\n\tif (parsed.mode === \"rpc\" && parsed.fileArgs.length > 0) {\n\t\tconsole.error(chalk.red(\"Error: @file arguments are not supported in RPC mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tvalidateForkFlags(parsed);\n\tvalidateSessionIdFlags(parsed);\n\n\t// Run migrations (pass cwd for project-local migrations)\n\tconst { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(cwd);\n\ttime(\"runMigrations\");\n\n\tconst startupSettingsManager = SettingsManager.create(cwd, agentDir);\n\treportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, \"startup session lookup\"));\n\n\t// Experimental first-time setup: theme choice and analytics opt-in.\n\t// Runs before any runtime services are created so the chosen settings apply everywhere.\n\tif (appMode === \"interactive\" && !parsed.help && parsed.listModels === undefined && shouldRunFirstTimeSetup()) {\n\t\tawait showFirstTimeSetup(startupSettingsManager);\n\t\ttime(\"firstTimeSetup\");\n\t}\n\n\t// Decide the final runtime cwd before creating cwd-bound runtime services.\n\t// --session and --resume may select a session from another project, so project-local\n\t// settings, resources, provider registrations, and models must be resolved only after\n\t// the target session cwd is known. The startup-cwd settings manager is used only for\n\t// sessionDir lookup during session selection.\n\tconst envSessionDir = process.env[ENV_SESSION_DIR];\n\tconst sessionDir =\n\t\t(parsed.sessionDir ? normalizePath(parsed.sessionDir) : undefined) ??\n\t\t(envSessionDir ? expandTildePath(envSessionDir) : undefined) ??\n\t\tstartupSettingsManager.getSessionDir();\n\tlet sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager);\n\tconst missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);\n\tif (missingSessionCwdIssue) {\n\t\tif (appMode === \"interactive\") {\n\t\t\tconst selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager);\n\t\t\tif (!selectedCwd) {\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\tsessionManager = SessionManager.open(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd);\n\t\t} else {\n\t\t\tconsole.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\tif (parsed.name !== undefined) {\n\t\tconst name = parsed.name.trim();\n\t\tif (!name) {\n\t\t\tconsole.error(chalk.red(\"Error: --name requires a non-empty value\"));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tsessionManager.appendSessionInfo(name);\n\t}\n\ttime(\"createSessionManager\");\n\n\tconst trustStore = new ProjectTrustStore(agentDir);\n\tconst sessionCwd = sessionManager.getCwd();\n\tconst autoTrustOnReloadCwd =\n\t\tparsed.projectTrustOverride === undefined && !hasTrustRequiringProjectResources(sessionCwd)\n\t\t\t? sessionCwd\n\t\t\t: undefined;\n\tconst trustPromptMode: AppMode = parsed.help || parsed.listModels !== undefined ? \"print\" : appMode;\n\tconst projectTrustByCwd = new Map<string, boolean>();\n\n\tconst resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);\n\tconst resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);\n\tconst resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);\n\tconst resolvedThemePaths = resolveCliPaths(cwd, parsed.themes);\n\tconst createRuntime: CreateAgentSessionRuntimeFactory = async ({\n\t\tcwd,\n\t\tagentDir,\n\t\tsessionManager,\n\t\tsessionStartEvent,\n\t\tprojectTrustContext,\n\t}) => {\n\t\tconst isInitialRuntime = sessionStartEvent === undefined;\n\t\tconst projectTrustDiagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\t\tconst cachedProjectTrust = projectTrustByCwd.get(cwd);\n\t\tconst hasTrustRequiringResources = hasTrustRequiringProjectResources(cwd);\n\t\tconst shouldResolveProjectTrust =\n\t\t\tparsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustRequiringResources;\n\t\tconst projectTrusted = shouldResolveProjectTrust\n\t\t\t? false\n\t\t\t: (cachedProjectTrust ??\n\t\t\t\tparsed.projectTrustOverride ??\n\t\t\t\t(!hasTrustRequiringResources || trustStore.get(cwd) === true));\n\t\tconst runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });\n\t\tconst services = await createAgentSessionServices({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tsessionId: sessionManager.getSessionId(),\n\t\t\tsettingsManager: runtimeSettingsManager,\n\t\t\tmodelRuntimeSignal: AbortSignal.timeout(15_000),\n\t\t\textensionFlagValues: parsed.unknownFlags,\n\t\t\tresourceLoaderReloadOptions: shouldResolveProjectTrust\n\t\t\t\t? {\n\t\t\t\t\t\tresolveProjectTrust: async ({ extensionsResult }) => {\n\t\t\t\t\t\t\tconst trusted = await resolveProjectTrusted({\n\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\ttrustStore,\n\t\t\t\t\t\t\t\ttrustOverride: parsed.projectTrustOverride,\n\t\t\t\t\t\t\t\tdefaultProjectTrust: startupSettingsManager.getDefaultProjectTrust(),\n\t\t\t\t\t\t\t\textensionsResult,\n\t\t\t\t\t\t\t\tprojectTrustContext:\n\t\t\t\t\t\t\t\t\tprojectTrustContext ??\n\t\t\t\t\t\t\t\t\tcreateProjectTrustContext({\n\t\t\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\t\t\tmode: isInitialRuntime ? trustPromptMode : appMode,\n\t\t\t\t\t\t\t\t\t\tsettingsManager: startupSettingsManager,\n\t\t\t\t\t\t\t\t\t\thasUI: isInitialRuntime && trustPromptMode === \"interactive\",\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\tonExtensionError: (message) => projectTrustDiagnostics.push({ type: \"warning\", message }),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tprojectTrustByCwd.set(cwd, trusted);\n\t\t\t\t\t\t\treturn trusted;\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\t\tresourceLoaderOptions: {\n\t\t\t\tadditionalExtensionPaths: resolvedExtensionPaths,\n\t\t\t\tadditionalSkillPaths: resolvedSkillPaths,\n\t\t\t\tadditionalPromptTemplatePaths: resolvedPromptTemplatePaths,\n\t\t\t\tadditionalThemePaths: resolvedThemePaths,\n\t\t\t\tnoExtensions: parsed.noExtensions,\n\t\t\t\tnoSkills: parsed.noSkills,\n\t\t\t\tnoPromptTemplates: parsed.noPromptTemplates,\n\t\t\t\tnoThemes: parsed.noThemes,\n\t\t\t\tnoContextFiles: parsed.noContextFiles,\n\t\t\t\tsystemPrompt: parsed.systemPrompt,\n\t\t\t\tappendSystemPrompt: parsed.appendSystemPrompt,\n\t\t\t\textensionFactories,\n\t\t\t},\n\t\t});\n\t\tconst { settingsManager, modelRuntime, resourceLoader } = services;\n\t\tconst permissionStore = new FilePermissionRuleStore({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tinitialRules: (parsed.allowedTools ?? []).map((toolName) => ({\n\t\t\t\tsource: \"cliArg\" as const,\n\t\t\t\ttoolName,\n\t\t\t\tbehavior: \"allow\" as const,\n\t\t\t})),\n\t\t});\n\t\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [\n\t\t\t...projectTrustDiagnostics,\n\t\t\t...services.diagnostics,\n\t\t\t...collectSettingsDiagnostics(settingsManager, \"runtime creation\"),\n\t\t\t...resourceLoader.getExtensions().errors.map(({ path, error }) => ({\n\t\t\t\ttype: \"error\" as const,\n\t\t\t\tmessage: `Failed to load extension \"${path}\": ${error}`,\n\t\t\t})),\n\t\t];\n\n\t\tconst modelPatterns = parsed.models ?? settingsManager.getEnabledModels();\n\t\tconst scopedModels =\n\t\t\tmodelPatterns && modelPatterns.length > 0\n\t\t\t\t? await resolveModelScope(modelPatterns, modelRuntime, { signal: AbortSignal.timeout(15_000) })\n\t\t\t\t: [];\n\t\tconst {\n\t\t\toptions: sessionOptions,\n\t\t\tcliThinkingFromModel,\n\t\t\tdiagnostics: sessionOptionDiagnostics,\n\t\t} = buildSessionOptions(\n\t\t\tparsed,\n\t\t\tscopedModels,\n\t\t\tsessionManager.buildSessionContext().messages.length > 0,\n\t\t\tmodelRuntime,\n\t\t\tsettingsManager,\n\t\t);\n\t\tdiagnostics.push(...sessionOptionDiagnostics);\n\n\t\tif (parsed.apiKey) {\n\t\t\tif (!sessionOptions.model) {\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tmessage: \"--api-key requires a model to be specified via --model, --provider/--model, or --models\",\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tawait modelRuntime.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);\n\t\t\t}\n\t\t}\n\n\t\tconst created = await createAgentSessionFromServices({\n\t\t\tservices,\n\t\t\tsessionManager,\n\t\t\tsessionStartEvent,\n\t\t\tmodel: sessionOptions.model,\n\t\t\tthinkingLevel: sessionOptions.thinkingLevel,\n\t\t\tscopedModels: sessionOptions.scopedModels,\n\t\t\ttools: sessionOptions.tools,\n\t\t\texcludeTools: sessionOptions.excludeTools,\n\t\t\tnoTools: sessionOptions.noTools,\n\t\t\tcustomTools: sessionOptions.customTools,\n\t\t\tpermissionGate: {\n\t\t\t\tstore: permissionStore,\n\t\t\t\tgetMode: async () =>\n\t\t\t\t\tresolveEffectiveMode(parsed.permissionMode, (await permissionStore.snapshot()).modesBySource),\n\t\t\t},\n\t\t});\n\t\tconst unclassifiedToolNames = created.session\n\t\t\t.getAllTools()\n\t\t\t.filter((tool) => tool.unclassified)\n\t\t\t.map((tool) => tool.name);\n\t\tif (unclassifiedToolNames.length > 0) {\n\t\t\tdiagnostics.push({\n\t\t\t\ttype: \"warning\",\n\t\t\t\tmessage: `Unclassified tool contracts (approval required): ${unclassifiedToolNames.join(\", \")}`,\n\t\t\t});\n\t\t}\n\t\tconst cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;\n\t\tif (created.session.model && cliThinkingOverride) {\n\t\t\tcreated.session.setThinkingLevel(created.session.thinkingLevel);\n\t\t}\n\n\t\treturn {\n\t\t\t...created,\n\t\t\tservices,\n\t\t\tdiagnostics,\n\t\t};\n\t};\n\ttime(\"createRuntime\");\n\tconst runtime = await createAgentSessionRuntime(createRuntime, {\n\t\tcwd: sessionManager.getCwd(),\n\t\tagentDir,\n\t\tsessionManager,\n\t});\n\ttime(\"createAgentSessionRuntime\");\n\tconst { services, session, modelFallbackMessage } = runtime;\n\tconst { settingsManager, modelRuntime, resourceLoader } = services;\n\tapplyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy);\n\tconfigureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());\n\n\tif (parsed.help) {\n\t\tconst extensionFlags = resourceLoader\n\t\t\t.getExtensions()\n\t\t\t.extensions.flatMap((extension) => Array.from(extension.flags.values()));\n\t\tprintHelp(extensionFlags);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.listModels !== undefined) {\n\t\tconst searchPattern = typeof parsed.listModels === \"string\" ? parsed.listModels : undefined;\n\t\tawait listModels(modelRuntime, searchPattern, AbortSignal.timeout(15_000));\n\t\tprocess.exit(0);\n\t}\n\n\t// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC\n\tlet stdinContent: string | undefined;\n\tif (appMode !== \"rpc\") {\n\t\tstdinContent = await readPipedStdin();\n\t\tif (stdinContent !== undefined && appMode === \"interactive\") {\n\t\t\tappMode = \"print\";\n\t\t}\n\t}\n\ttime(\"readPipedStdin\");\n\n\tconst { initialMessage, initialImages } = await prepareInitialMessage(\n\t\tparsed,\n\t\tsettingsManager.getImageAutoResize(),\n\t\tstdinContent,\n\t);\n\ttime(\"prepareInitialMessage\");\n\tinitTheme(settingsManager.getTheme(), appMode === \"interactive\");\n\ttime(\"initTheme\");\n\n\t// Show deprecation warnings in interactive mode\n\tif (appMode === \"interactive\" && deprecationWarnings.length > 0) {\n\t\tawait showDeprecationWarnings(deprecationWarnings);\n\t}\n\n\ttime(\"resolveModelScope\");\n\treportDiagnostics(runtime.diagnostics);\n\tif (runtime.diagnostics.some((diagnostic) => diagnostic.type === \"error\")) {\n\t\tif (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes(\"Failed to load extension\"))) {\n\t\t\tconsole.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));\n\t\t}\n\t\tprocess.exit(1);\n\t}\n\ttime(\"createAgentSession\");\n\n\tif (appMode !== \"interactive\" && !session.model) {\n\t\tconsole.error(chalk.red(formatNoModelsAvailableMessage()));\n\t\tprocess.exit(1);\n\t}\n\n\tconst startupBenchmark = isTruthyEnvFlag(getApexEnvironment(\"APEX_CODE_STARTUP_BENCHMARK\"));\n\tif (startupBenchmark && appMode !== \"interactive\") {\n\t\tconsole.error(chalk.red(\"Error: APEX_CODE_STARTUP_BENCHMARK only supports interactive mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\t// RPC refreshes catalogs here in the background; interactive mode starts its refresh after TUI initialization.\n\tif (!offlineMode && appMode === \"rpc\") {\n\t\tconst controller = new AbortController();\n\t\tconst timeout = setTimeout(() => controller.abort(), 15_000);\n\t\tvoid modelRuntime\n\t\t\t.refresh({ signal: controller.signal })\n\t\t\t.catch(() => {})\n\t\t\t.finally(() => clearTimeout(timeout));\n\t}\n\n\tif (appMode === \"rpc\") {\n\t\tprintTimings();\n\t\tawait runRpcMode(runtime);\n\t} else if (appMode === \"interactive\") {\n\t\tconst interactiveMode = new InteractiveMode(runtime, {\n\t\t\tmigratedProviders,\n\t\t\tmodelFallbackMessage,\n\t\t\tautoTrustOnReloadCwd,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\tinitialMessages: parsed.messages,\n\t\t\tverbose: parsed.verbose,\n\t\t\ttuiMode: parsed.tuiMode,\n\t\t});\n\t\tif (startupBenchmark) {\n\t\t\tawait interactiveMode.init();\n\t\t\ttime(\"interactiveMode.init\");\n\t\t\t// Give the TUI's stdin handler a brief chance to consume terminal query replies\n\t\t\t// (Kitty keyboard protocol, device attributes, cell size) before restoring the terminal.\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 150));\n\t\t\tinteractiveMode.stop();\n\t\t\tstopThemeWatcher();\n\t\t\tprintTimings();\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tprintTimings();\n\t\tawait interactiveMode.run();\n\t} else {\n\t\tprintTimings();\n\t\tconst exitCode = await runPrintMode(runtime, {\n\t\t\tmode: toPrintOutputMode(appMode),\n\t\t\tmessages: parsed.messages,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t});\n\t\tstopThemeWatcher();\n\t\trestoreStdout();\n\t\tif (exitCode !== 0) {\n\t\t\tprocess.exitCode = exitCode;\n\t\t}\n\t\treturn;\n\t}\n}\n"]}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyCH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AA0gBlE,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,eAAe,EAAE,CAAC;CACvC;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBA6b/D","sourcesContent":["/**\n * Main entry point for the coding agent CLI.\n *\n * This file handles CLI argument parsing and translates them into\n * createAgentSession() options. The SDK does the heavy lifting.\n */\n\nimport { createInterface } from \"node:readline\";\nimport { type ImageContent, modelsAreEqual } from \"@earendil-works/pi-ai\";\nimport chalk from \"chalk\";\nimport { type Args, type Mode, parseArgs, printHelp } from \"./cli/args.ts\";\nimport {\n\ttype AuthCheckResult,\n\tcheckProviderAuth,\n\tcreateAuthCheckModelRuntime,\n\tgetProviderCredential,\n} from \"./cli/auth-check.ts\";\nimport {\n\ttype AuthCommand,\n\tAuthCommandError,\n\tgetAuthCommandName,\n\tgetAuthCommandUsage,\n\tisAuthCommandHelp,\n\tparseAuthCommand,\n\tprintAuthCommandHelp,\n\tvalidateAuthCommandArgs,\n} from \"./cli/auth-command.ts\";\nimport { runCostCommand } from \"./cli/cost-command.ts\";\nimport { resolveCredentialForPrint } from \"./cli/credential-print.ts\";\nimport { processFileArguments } from \"./cli/file-processor.ts\";\nimport { buildInitialMessage } from \"./cli/initial-message.ts\";\nimport { listModels } from \"./cli/list-models.ts\";\nimport { createProjectTrustContext } from \"./cli/project-trust.ts\";\nimport { selectSession } from \"./cli/session-picker.ts\";\nimport { shouldRunFirstTimeSetup, showFirstTimeSetup, showStartupSelector } from \"./cli/startup-ui.ts\";\nimport { APP_NAME, ENV_SESSION_DIR, expandTildePath, getAgentDir, getPackageDir, VERSION } from \"./config.ts\";\nimport { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from \"./core/agent-session-runtime.ts\";\nimport {\n\ttype AgentSessionRuntimeDiagnostic,\n\tcreateAgentSessionFromServices,\n\tcreateAgentSessionServices,\n} from \"./core/agent-session-services.ts\";\nimport { formatNoModelsAvailableMessage } from \"./core/auth-guidance.ts\";\nimport { AuthStorage, ReadOnlyAuthStorage } from \"./core/auth-storage.ts\";\nimport { getApexEnvironment, setApexEnvironment } from \"./core/environment.ts\";\nimport { exportFromFile } from \"./core/export-html/index.ts\";\nimport type { InlineExtension } from \"./core/extensions/types.ts\";\nimport { applyHttpProxySettings, configureHttpDispatcher } from \"./core/http-dispatcher.ts\";\nimport { resolveCliModel, resolveModelScope, type ScopedModel } from \"./core/model-resolver.ts\";\nimport { ModelRuntime } from \"./core/model-runtime.ts\";\nimport { restoreStdout, takeOverStdout } from \"./core/output-guard.ts\";\nimport { resolveEffectiveMode, resolvePermissionModeForStartup } from \"./core/permissions/startup.ts\";\nimport { FilePermissionRuleStore } from \"./core/permissions/store.ts\";\nimport { type AppMode, resolveProjectTrusted } from \"./core/project-trust.ts\";\nimport type { CreateAgentSessionOptions } from \"./core/sdk.ts\";\nimport {\n\tformatMissingSessionCwdPrompt,\n\tgetMissingSessionCwdIssue,\n\tMissingSessionCwdError,\n\ttype SessionCwdIssue,\n} from \"./core/session-cwd.ts\";\nimport { assertValidSessionId, SessionManager } from \"./core/session-manager.ts\";\nimport { SettingsManager } from \"./core/settings-manager.ts\";\nimport { printTimings, resetTimings, time } from \"./core/timings.ts\";\nimport { hasTrustRequiringProjectResources, ProjectTrustStore } from \"./core/trust-manager.ts\";\nimport { builtInExtensions } from \"./extensions/index.ts\";\nimport { runMigrations, showDeprecationWarnings } from \"./migrations.ts\";\nimport { InteractiveMode, runPrintMode, runRpcMode } from \"./modes/index.ts\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.ts\";\nimport { handleConfigCommand, handlePackageCommand } from \"./package-manager-cli.ts\";\nimport { isLocalPath, normalizePath, resolvePath } from \"./utils/paths.ts\";\nimport { cleanupWindowsSelfUpdateQuarantine } from \"./utils/windows-self-update.ts\";\n\nconst EXTENSION_LOAD_FAILURE_HINT = 'Hint: Start without extensions using \"apex-code -ne\".';\n\n/**\n * Read all content from piped stdin.\n * Returns undefined if stdin is a TTY (interactive terminal).\n */\nasync function readPipedStdin(): Promise<string | undefined> {\n\t// If stdin is a TTY, we're running interactively - don't read stdin\n\tif (process.stdin.isTTY) {\n\t\treturn undefined;\n\t}\n\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tprocess.stdin.setEncoding(\"utf8\");\n\t\tprocess.stdin.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tprocess.stdin.on(\"end\", () => {\n\t\t\tresolve(data.trim() || undefined);\n\t\t});\n\t\tprocess.stdin.resume();\n\t});\n}\n\nfunction collectSettingsDiagnostics(\n\tsettingsManager: SettingsManager,\n\tcontext: string,\n): AgentSessionRuntimeDiagnostic[] {\n\treturn settingsManager.drainErrors().map(({ scope, error }) => ({\n\t\ttype: \"warning\",\n\t\tmessage: `(${context}, ${scope} settings) ${error.message}`,\n\t}));\n}\n\nfunction reportDiagnostics(diagnostics: readonly AgentSessionRuntimeDiagnostic[]): void {\n\tfor (const diagnostic of diagnostics) {\n\t\tconst color = diagnostic.type === \"error\" ? chalk.red : diagnostic.type === \"warning\" ? chalk.yellow : chalk.dim;\n\t\tconst prefix = diagnostic.type === \"error\" ? \"Error: \" : diagnostic.type === \"warning\" ? \"Warning: \" : \"\";\n\t\tconsole.error(color(`${prefix}${diagnostic.message}`));\n\t}\n}\n\nfunction isTruthyEnvFlag(value: string | undefined): boolean {\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\nfunction resolveAppMode(parsed: Args, stdinIsTTY: boolean, stdoutIsTTY: boolean): AppMode {\n\tif (parsed.mode === \"rpc\") {\n\t\treturn \"rpc\";\n\t}\n\tif (parsed.mode === \"json\") {\n\t\treturn \"json\";\n\t}\n\tif (parsed.print || !stdinIsTTY || !stdoutIsTTY) {\n\t\treturn \"print\";\n\t}\n\treturn \"interactive\";\n}\n\nfunction toPrintOutputMode(appMode: AppMode): Exclude<Mode, \"rpc\"> {\n\treturn appMode === \"json\" ? \"json\" : \"text\";\n}\n\nfunction isPlainRuntimeMetadataCommand(parsed: Args): boolean {\n\treturn !parsed.print && parsed.mode === undefined && (parsed.help === true || parsed.listModels !== undefined);\n}\n\nasync function runAuthCommand(args: string[]): Promise<boolean> {\n\tif (isAuthCommandHelp(args)) {\n\t\tprintAuthCommandHelp();\n\t\treturn true;\n\t}\n\n\tlet command: AuthCommand | undefined;\n\ttry {\n\t\tcommand = parseAuthCommand(args);\n\t} catch (error) {\n\t\tconst message = error instanceof AuthCommandError ? error.message : \"Failed to parse auth command\";\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exitCode = 1;\n\t\treturn true;\n\t}\n\tif (!command) return false;\n\n\tconst parsed = parseArgs(command.args);\n\tif (parsed.unknownFlags.size > 0) {\n\t\tconst option = parsed.unknownFlags.keys().next().value;\n\t\tconsole.error(chalk.red(`Unknown option --${option} for \"${getAuthCommandName(command.kind)}\".`));\n\t\tconsole.error(chalk.dim(`Use \"${APP_NAME} --help\" or \"${getAuthCommandUsage(command.kind)}\".`));\n\t\tprocess.exitCode = 1;\n\t\treturn true;\n\t}\n\ttry {\n\t\tif (parsed.diagnostics.length > 0) {\n\t\t\tthrow new AuthCommandError(parsed.diagnostics.map((diagnostic) => diagnostic.message).join(\"\\n\"));\n\t\t}\n\t\tif (command.kind !== \"check\") {\n\t\t\tconst signal = AbortSignal.timeout(15_000);\n\t\t\tconst modelRuntime = await ModelRuntime.create({ allowModelNetwork: false, signal });\n\t\t\tconst credential = await resolveCredentialForPrint(\n\t\t\t\tparsed,\n\t\t\t\tmodelRuntime,\n\t\t\t\tcommand.kind,\n\t\t\t\tcommand.minExpiryMs,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tprocess.stdout.write(`${credential}\\n`);\n\t\t\treturn true;\n\t\t}\n\n\t\tconst requestedAuth = validateAuthCommandArgs(parsed, command.kind);\n\t\tlet result: AuthCheckResult;\n\t\tlet credential: string | undefined;\n\t\ttry {\n\t\t\tconst credentials = command.noRefresh ? new ReadOnlyAuthStorage() : AuthStorage.create();\n\t\t\tconst modelRuntime = await createAuthCheckModelRuntime(credentials);\n\t\t\tresult = await checkProviderAuth(parsed, modelRuntime, { refresh: !command.noRefresh });\n\t\t\tif (command.credentials && result.status === \"ready\") {\n\t\t\t\tcredential = await getProviderCredential(result.provider, modelRuntime, credentials, {\n\t\t\t\t\trefresh: !command.noRefresh,\n\t\t\t\t});\n\t\t\t\tif (!credential) {\n\t\t\t\t\tresult = { status: \"not_ready\", provider: result.provider, reason: \"credential_not_available\" };\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\tresult = {\n\t\t\t\tstatus: \"invalid\",\n\t\t\t\tprovider: requestedAuth.provider ?? requestedAuth.model!,\n\t\t\t\treason: \"invalid_state\",\n\t\t\t};\n\t\t}\n\t\tconst output = command.json\n\t\t\t? JSON.stringify({ ...result, ...(credential ? { credentials: credential } : {}) })\n\t\t\t: (credential ?? result.status);\n\t\tprocess.stdout.write(`${output}\\n`);\n\t\tprocess.exitCode = result.status === \"ready\" ? 0 : result.status === \"not_ready\" ? 1 : 2;\n\t} catch (error) {\n\t\tconst message = error instanceof AuthCommandError ? error.message : \"Failed to resolve credential\";\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exitCode = command.kind === \"check\" ? 2 : 1;\n\t}\n\treturn true;\n}\n\nasync function prepareInitialMessage(\n\tparsed: Args,\n\tautoResizeImages: boolean,\n\tstdinContent?: string,\n): Promise<{\n\tinitialMessage?: string;\n\tinitialImages?: ImageContent[];\n}> {\n\tif (parsed.fileArgs.length === 0) {\n\t\treturn buildInitialMessage({ parsed, stdinContent });\n\t}\n\n\tconst { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });\n\treturn buildInitialMessage({\n\t\tparsed,\n\t\tfileText: text,\n\t\tfileImages: images,\n\t\tstdinContent,\n\t});\n}\n\n/** Result from resolving a session argument */\ntype ResolvedSession =\n\t| { type: \"path\"; path: string } // Direct file path\n\t| { type: \"local\"; path: string } // Found in current project\n\t| { type: \"global\"; path: string; cwd: string } // Found in different project\n\t| { type: \"not_found\"; arg: string }; // Not found anywhere\n\n/**\n * Resolve a session argument to a file path.\n * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.\n */\nasync function findLocalSessionByExactId(\n\tsessionId: string,\n\tcwd: string,\n\tsessionDir?: string,\n): Promise<{ type: \"local\"; path: string } | undefined> {\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatch = localSessions.find((s) => s.id === sessionId);\n\treturn localMatch ? { type: \"local\", path: localMatch.path } : undefined;\n}\n\nasync function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise<ResolvedSession> {\n\t// If it looks like a file path, resolve it before handing it to the session manager.\n\tif (sessionArg.includes(\"/\") || sessionArg.includes(\"\\\\\") || sessionArg.endsWith(\".jsonl\")) {\n\t\treturn { type: \"path\", path: resolvePath(sessionArg, cwd) };\n\t}\n\n\t// Try to match as session ID in current project first\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatch =\n\t\tlocalSessions.find((s) => s.id === sessionArg) ?? localSessions.find((s) => s.id.startsWith(sessionArg));\n\n\tif (localMatch) {\n\t\treturn { type: \"local\", path: localMatch.path };\n\t}\n\n\t// Try global search across all projects\n\tconst allSessions = await SessionManager.listAll(sessionDir);\n\tconst globalMatch =\n\t\tallSessions.find((s) => s.id === sessionArg) ?? allSessions.find((s) => s.id.startsWith(sessionArg));\n\n\tif (globalMatch) {\n\t\treturn { type: \"global\", path: globalMatch.path, cwd: globalMatch.cwd };\n\t}\n\n\t// Not found anywhere\n\treturn { type: \"not_found\", arg: sessionArg };\n}\n\n/** Prompt user for yes/no confirmation */\nasync function promptConfirm(message: string): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst rl = createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\t\trl.question(`${message} [y/N] `, (answer) => {\n\t\t\trl.close();\n\t\t\tresolve(answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\");\n\t\t});\n\t});\n}\n\nfunction validateForkFlags(parsed: Args): void {\n\tif (!parsed.fork) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t\tparsed.noSession ? \"--no-session\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction validateSessionIdFlags(parsed: Args): void {\n\tif (parsed.sessionId === undefined) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --session-id cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n\n\ttry {\n\t\tassertValidSessionId(parsed.sessionId);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction openSessionOrExit(path: string, sessionDir?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.open(path, sessionDir);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction forkSessionOrExit(sourcePath: string, cwd: string, sessionDir?: string, sessionId?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.forkFrom(sourcePath, cwd, sessionDir, { id: sessionId });\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nasync function createSessionManager(\n\tparsed: Args,\n\tcwd: string,\n\tsessionDir: string | undefined,\n\tsettingsManager: SettingsManager,\n): Promise<SessionManager> {\n\tif (parsed.noSession || parsed.help || parsed.listModels !== undefined) {\n\t\treturn SessionManager.inMemory(cwd, parsed.sessionId !== undefined ? { id: parsed.sessionId } : undefined);\n\t}\n\n\tif (parsed.fork) {\n\t\tif (parsed.sessionId) {\n\t\t\tconst existingTarget = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);\n\t\t\tif (existingTarget) {\n\t\t\t\tconsole.error(chalk.red(`Session already exists with id '${parsed.sessionId}'`));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\n\t\tconst resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\tcase \"global\":\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir, parsed.sessionId);\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.session) {\n\t\tconst resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\t\treturn openSessionOrExit(resolved.path, sessionDir);\n\n\t\t\tcase \"global\": {\n\t\t\t\tconsole.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));\n\t\t\t\tconst shouldFork = await promptConfirm(\"Fork this session into current directory?\");\n\t\t\t\tif (!shouldFork) {\n\t\t\t\t\tconsole.log(chalk.dim(\"Aborted.\"));\n\t\t\t\t\tprocess.exit(0);\n\t\t\t\t}\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\t\t\t}\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.resume) {\n\t\ttry {\n\t\t\tconst selectedPath = await selectSession(\n\t\t\t\t(onProgress) => SessionManager.list(cwd, sessionDir, onProgress),\n\t\t\t\t(onProgress) => SessionManager.listAll(sessionDir, onProgress),\n\t\t\t\tsettingsManager,\n\t\t\t);\n\t\t\tif (!selectedPath) {\n\t\t\t\tconsole.log(chalk.dim(\"No session selected\"));\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\treturn SessionManager.open(selectedPath, sessionDir);\n\t\t} finally {\n\t\t\tstopThemeWatcher();\n\t\t}\n\t}\n\n\tif (parsed.continue) {\n\t\treturn SessionManager.continueRecent(cwd, sessionDir);\n\t}\n\n\tif (parsed.sessionId) {\n\t\tconst existingSession = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);\n\t\tif (existingSession) {\n\t\t\treturn SessionManager.open(existingSession.path, sessionDir);\n\t\t}\n\t\tconsole.error(\n\t\t\tchalk.yellow(\n\t\t\t\t`Warning: No project session found with id '${parsed.sessionId}'; creating a new session with that id.`,\n\t\t\t),\n\t\t);\n\t}\n\n\treturn SessionManager.create(cwd, sessionDir, { id: parsed.sessionId });\n}\n\nfunction buildSessionOptions(\n\tparsed: Args,\n\tscopedModels: ScopedModel[],\n\thasExistingSession: boolean,\n\tmodelRuntime: ModelRuntime,\n\tsettingsManager: SettingsManager,\n): {\n\toptions: CreateAgentSessionOptions;\n\tcliThinkingFromModel: boolean;\n\tdiagnostics: AgentSessionRuntimeDiagnostic[];\n} {\n\tconst options: CreateAgentSessionOptions = {};\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tlet cliThinkingFromModel = false;\n\n\t// Model from CLI\n\t// - supports --provider <name> --model <pattern>\n\t// - supports --model <provider>/<pattern>\n\tif (parsed.model) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider: parsed.provider,\n\t\t\tcliModel: parsed.model,\n\t\t\tcliThinking: parsed.thinking,\n\t\t\tmodelRuntime,\n\t\t});\n\t\tif (resolved.warning) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: resolved.warning });\n\t\t}\n\t\tif (resolved.error) {\n\t\t\tdiagnostics.push({ type: \"error\", message: resolved.error });\n\t\t}\n\t\tif (resolved.model) {\n\t\t\toptions.model = resolved.model;\n\t\t\t// Allow \"--model <pattern>:<thinking>\" as a shorthand.\n\t\t\t// Explicit --thinking still takes precedence (applied later).\n\t\t\tif (!parsed.thinking && resolved.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = resolved.thinkingLevel;\n\t\t\t\tcliThinkingFromModel = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!options.model && scopedModels.length > 0 && !hasExistingSession) {\n\t\t// Check if saved default is in scoped models - use it if so, otherwise first scoped model\n\t\tconst savedProvider = settingsManager.getDefaultProvider();\n\t\tconst savedModelId = settingsManager.getDefaultModel();\n\t\tconst savedModel = savedProvider && savedModelId ? modelRuntime.getModel(savedProvider, savedModelId) : undefined;\n\t\tconst savedInScope = savedModel ? scopedModels.find((sm) => modelsAreEqual(sm.model, savedModel)) : undefined;\n\n\t\tif (savedInScope) {\n\t\t\toptions.model = savedInScope.model;\n\t\t\t// Use thinking level from scoped model config if explicitly set\n\t\t\tif (!parsed.thinking && savedInScope.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = savedInScope.thinkingLevel;\n\t\t\t}\n\t\t} else {\n\t\t\toptions.model = scopedModels[0].model;\n\t\t\t// Use thinking level from first scoped model if explicitly set\n\t\t\tif (!parsed.thinking && scopedModels[0].thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = scopedModels[0].thinkingLevel;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Thinking level from CLI (takes precedence over scoped model thinking levels set above)\n\tif (parsed.thinking) {\n\t\toptions.thinkingLevel = parsed.thinking;\n\t}\n\n\t// Scoped models for Ctrl+P cycling\n\t// Keep thinking level undefined when not explicitly set in the model pattern.\n\t// Undefined means \"inherit current session thinking level\" during cycling.\n\tif (scopedModels.length > 0) {\n\t\toptions.scopedModels = scopedModels.map((sm) => ({\n\t\t\tmodel: sm.model,\n\t\t\tthinkingLevel: sm.thinkingLevel,\n\t\t}));\n\t}\n\n\t// API key from CLI - set as a non-persistent runtime override\n\t// (handled by caller before createAgentSession)\n\n\t// Tools\n\tif (parsed.noTools) {\n\t\toptions.noTools = \"all\";\n\t} else if (parsed.noBuiltinTools) {\n\t\toptions.noTools = \"builtin\";\n\t}\n\tif (parsed.tools) {\n\t\toptions.tools = [...parsed.tools];\n\t}\n\tif (parsed.excludeTools) {\n\t\toptions.excludeTools = [...parsed.excludeTools];\n\t}\n\n\treturn { options, cliThinkingFromModel, diagnostics };\n}\n\nfunction resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined {\n\treturn paths?.map((value) => (isLocalPath(value) ? resolvePath(value, cwd) : value));\n}\n\nasync function promptForMissingSessionCwd(\n\tissue: SessionCwdIssue,\n\tsettingsManager: SettingsManager,\n): Promise<string | undefined> {\n\treturn showStartupSelector(settingsManager, formatMissingSessionCwdPrompt(issue), [\n\t\t{ label: \"Continue\", value: issue.fallbackCwd },\n\t\t{ label: \"Cancel\", value: undefined },\n\t]);\n}\n\nexport interface MainOptions {\n\textensionFactories?: InlineExtension[];\n}\n\nexport async function main(args: string[], options?: MainOptions) {\n\tresetTimings();\n\tconst extensionFactories = [...builtInExtensions, ...(options?.extensionFactories ?? [])];\n\tconst offlineMode = args.includes(\"--offline\") || isTruthyEnvFlag(getApexEnvironment(\"APEX_CODE_OFFLINE\"));\n\tif (offlineMode) {\n\t\tsetApexEnvironment(\"APEX_CODE_OFFLINE\", \"1\");\n\t\tsetApexEnvironment(\"APEX_CODE_SKIP_VERSION_CHECK\", \"1\");\n\t}\n\n\tif (await runAuthCommand(args)) {\n\t\treturn;\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\tcleanupWindowsSelfUpdateQuarantine(getPackageDir());\n\t}\n\n\tconst cwd = process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst bootstrapSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tapplyHttpProxySettings(bootstrapSettingsManager.getGlobalSettings().httpProxy);\n\tconfigureHttpDispatcher();\n\n\tif (args[0] === \"cost\") {\n\t\tawait runCostCommand(agentDir, args.slice(1));\n\t\treturn;\n\t}\n\n\tif (await handlePackageCommand(args, { extensionFactories })) {\n\t\tconst exitCode = process.exitCode ?? 0;\n\t\tif (process.platform === \"win32\" && exitCode === 0 && args[0] === \"update\") {\n\t\t\t// We normally prefer process.exit(0) for package commands so bad extensions cannot keep\n\t\t\t// one-shot commands alive. On Windows, Node can assert after fetch() if process.exit(0)\n\t\t\t// runs during teardown; let successful `pi update` drain naturally instead.\n\t\t\t// https://github.com/nodejs/node/issues/56645\n\t\t\treturn;\n\t\t}\n\t\tprocess.exit(exitCode);\n\t\treturn;\n\t}\n\n\tif (await handleConfigCommand(args, { extensionFactories })) {\n\t\treturn;\n\t}\n\n\tconst parsed = parseArgs(args);\n\tif (parsed.diagnostics.length > 0) {\n\t\tfor (const d of parsed.diagnostics) {\n\t\t\tconst color = d.type === \"error\" ? chalk.red : chalk.yellow;\n\t\t\tconsole.error(color(`${d.type === \"error\" ? \"Error\" : \"Warning\"}: ${d.message}`));\n\t\t}\n\t\tif (parsed.diagnostics.some((d) => d.type === \"error\")) {\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"parseArgs\");\n\n\tif (parsed.version) {\n\t\tconsole.log(VERSION);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.export) {\n\t\tlet result: string;\n\t\ttry {\n\t\t\tconst outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;\n\t\t\tresult = await exportFromFile(parsed.export, outputPath);\n\t\t} catch (error: unknown) {\n\t\t\tconst message = error instanceof Error ? error.message : \"Failed to export session\";\n\t\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconsole.log(`Exported to: ${result}`);\n\t\tprocess.exit(0);\n\t}\n\n\tlet appMode = resolveAppMode(parsed, process.stdin.isTTY, process.stdout.isTTY);\n\tconst shouldTakeOverStdout = appMode !== \"interactive\" && !isPlainRuntimeMetadataCommand(parsed);\n\tif (shouldTakeOverStdout) {\n\t\ttakeOverStdout();\n\t}\n\n\t// A headless caller cannot answer an `ask`; require an explicit operator\n\t// decision before creating a session. Metadata commands never create one.\n\tconst permissionModeStartup = resolvePermissionModeForStartup({\n\t\tinteractive: appMode === \"interactive\",\n\t\trequestedMode: parsed.permissionMode,\n\t});\n\tif (!permissionModeStartup.ok && !parsed.help && parsed.listModels === undefined) {\n\t\tconsole.error(chalk.red(`Error: ${permissionModeStartup.message}`));\n\t\tprocess.exit(1);\n\t}\n\n\tif (parsed.mode === \"rpc\" && parsed.fileArgs.length > 0) {\n\t\tconsole.error(chalk.red(\"Error: @file arguments are not supported in RPC mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tvalidateForkFlags(parsed);\n\tvalidateSessionIdFlags(parsed);\n\n\t// Run migrations (pass cwd for project-local migrations)\n\tconst { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(cwd);\n\ttime(\"runMigrations\");\n\n\tconst startupSettingsManager = SettingsManager.create(cwd, agentDir);\n\treportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, \"startup session lookup\"));\n\n\t// Experimental first-time setup: theme choice and analytics opt-in.\n\t// Runs before any runtime services are created so the chosen settings apply everywhere.\n\tif (appMode === \"interactive\" && !parsed.help && parsed.listModels === undefined && shouldRunFirstTimeSetup()) {\n\t\tawait showFirstTimeSetup(startupSettingsManager);\n\t\ttime(\"firstTimeSetup\");\n\t}\n\n\t// Decide the final runtime cwd before creating cwd-bound runtime services.\n\t// --session and --resume may select a session from another project, so project-local\n\t// settings, resources, provider registrations, and models must be resolved only after\n\t// the target session cwd is known. The startup-cwd settings manager is used only for\n\t// sessionDir lookup during session selection.\n\tconst envSessionDir = process.env[ENV_SESSION_DIR];\n\tconst sessionDir =\n\t\t(parsed.sessionDir ? normalizePath(parsed.sessionDir) : undefined) ??\n\t\t(envSessionDir ? expandTildePath(envSessionDir) : undefined) ??\n\t\tstartupSettingsManager.getSessionDir();\n\tlet sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager);\n\tconst missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);\n\tif (missingSessionCwdIssue) {\n\t\tif (appMode === \"interactive\") {\n\t\t\tconst selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager);\n\t\t\tif (!selectedCwd) {\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\tsessionManager = SessionManager.open(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd);\n\t\t} else {\n\t\t\tconsole.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\tif (parsed.name !== undefined) {\n\t\tconst name = parsed.name.trim();\n\t\tif (!name) {\n\t\t\tconsole.error(chalk.red(\"Error: --name requires a non-empty value\"));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tsessionManager.appendSessionInfo(name);\n\t}\n\ttime(\"createSessionManager\");\n\n\tconst trustStore = new ProjectTrustStore(agentDir);\n\tconst sessionCwd = sessionManager.getCwd();\n\tconst autoTrustOnReloadCwd =\n\t\tparsed.projectTrustOverride === undefined && !hasTrustRequiringProjectResources(sessionCwd)\n\t\t\t? sessionCwd\n\t\t\t: undefined;\n\tconst trustPromptMode: AppMode = parsed.help || parsed.listModels !== undefined ? \"print\" : appMode;\n\tconst projectTrustByCwd = new Map<string, boolean>();\n\n\tconst resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);\n\tconst resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);\n\tconst resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);\n\tconst resolvedThemePaths = resolveCliPaths(cwd, parsed.themes);\n\tconst createRuntime: CreateAgentSessionRuntimeFactory = async ({\n\t\tcwd,\n\t\tagentDir,\n\t\tsessionManager,\n\t\tsessionStartEvent,\n\t\tprojectTrustContext,\n\t}) => {\n\t\tconst isInitialRuntime = sessionStartEvent === undefined;\n\t\tconst projectTrustDiagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\t\tconst cachedProjectTrust = projectTrustByCwd.get(cwd);\n\t\tconst hasTrustRequiringResources = hasTrustRequiringProjectResources(cwd);\n\t\tconst shouldResolveProjectTrust =\n\t\t\tparsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustRequiringResources;\n\t\tconst projectTrusted = shouldResolveProjectTrust\n\t\t\t? false\n\t\t\t: (cachedProjectTrust ??\n\t\t\t\tparsed.projectTrustOverride ??\n\t\t\t\t(!hasTrustRequiringResources || trustStore.get(cwd) === true));\n\t\tconst runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });\n\t\tconst services = await createAgentSessionServices({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tsessionId: sessionManager.getSessionId(),\n\t\t\tsettingsManager: runtimeSettingsManager,\n\t\t\tmodelRuntimeSignal: AbortSignal.timeout(15_000),\n\t\t\textensionFlagValues: parsed.unknownFlags,\n\t\t\tresourceLoaderReloadOptions: shouldResolveProjectTrust\n\t\t\t\t? {\n\t\t\t\t\t\tresolveProjectTrust: async ({ extensionsResult }) => {\n\t\t\t\t\t\t\tconst trusted = await resolveProjectTrusted({\n\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\ttrustStore,\n\t\t\t\t\t\t\t\ttrustOverride: parsed.projectTrustOverride,\n\t\t\t\t\t\t\t\tdefaultProjectTrust: startupSettingsManager.getDefaultProjectTrust(),\n\t\t\t\t\t\t\t\textensionsResult,\n\t\t\t\t\t\t\t\tprojectTrustContext:\n\t\t\t\t\t\t\t\t\tprojectTrustContext ??\n\t\t\t\t\t\t\t\t\tcreateProjectTrustContext({\n\t\t\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\t\t\tmode: isInitialRuntime ? trustPromptMode : appMode,\n\t\t\t\t\t\t\t\t\t\tsettingsManager: startupSettingsManager,\n\t\t\t\t\t\t\t\t\t\thasUI: isInitialRuntime && trustPromptMode === \"interactive\",\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\tonExtensionError: (message) => projectTrustDiagnostics.push({ type: \"warning\", message }),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tprojectTrustByCwd.set(cwd, trusted);\n\t\t\t\t\t\t\treturn trusted;\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\t\tresourceLoaderOptions: {\n\t\t\t\tadditionalExtensionPaths: resolvedExtensionPaths,\n\t\t\t\tadditionalSkillPaths: resolvedSkillPaths,\n\t\t\t\tadditionalPromptTemplatePaths: resolvedPromptTemplatePaths,\n\t\t\t\tadditionalThemePaths: resolvedThemePaths,\n\t\t\t\tnoExtensions: parsed.noExtensions,\n\t\t\t\tnoSkills: parsed.noSkills,\n\t\t\t\tnoPromptTemplates: parsed.noPromptTemplates,\n\t\t\t\tnoThemes: parsed.noThemes,\n\t\t\t\tnoContextFiles: parsed.noContextFiles,\n\t\t\t\tsystemPrompt: parsed.systemPrompt,\n\t\t\t\tappendSystemPrompt: parsed.appendSystemPrompt,\n\t\t\t\textensionFactories,\n\t\t\t},\n\t\t});\n\t\tconst { settingsManager, modelRuntime, resourceLoader } = services;\n\t\tconst permissionStore = new FilePermissionRuleStore({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tinitialRules: (parsed.allowedTools ?? []).map((toolName) => ({\n\t\t\t\tsource: \"cliArg\" as const,\n\t\t\t\ttoolName,\n\t\t\t\tbehavior: \"allow\" as const,\n\t\t\t})),\n\t\t});\n\t\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [\n\t\t\t...projectTrustDiagnostics,\n\t\t\t...services.diagnostics,\n\t\t\t...collectSettingsDiagnostics(settingsManager, \"runtime creation\"),\n\t\t\t...resourceLoader.getExtensions().errors.map(({ path, error }) => ({\n\t\t\t\ttype: \"error\" as const,\n\t\t\t\tmessage: `Failed to load extension \"${path}\": ${error}`,\n\t\t\t})),\n\t\t];\n\n\t\tconst modelPatterns = parsed.models ?? settingsManager.getEnabledModels();\n\t\tconst scopedModels =\n\t\t\tmodelPatterns && modelPatterns.length > 0\n\t\t\t\t? await resolveModelScope(modelPatterns, modelRuntime, { signal: AbortSignal.timeout(15_000) })\n\t\t\t\t: [];\n\t\tconst {\n\t\t\toptions: sessionOptions,\n\t\t\tcliThinkingFromModel,\n\t\t\tdiagnostics: sessionOptionDiagnostics,\n\t\t} = buildSessionOptions(\n\t\t\tparsed,\n\t\t\tscopedModels,\n\t\t\tsessionManager.buildSessionContext().messages.length > 0,\n\t\t\tmodelRuntime,\n\t\t\tsettingsManager,\n\t\t);\n\t\tdiagnostics.push(...sessionOptionDiagnostics);\n\n\t\tif (parsed.apiKey) {\n\t\t\tif (!sessionOptions.model) {\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tmessage: \"--api-key requires a model to be specified via --model, --provider/--model, or --models\",\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tawait modelRuntime.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);\n\t\t\t}\n\t\t}\n\n\t\tconst created = await createAgentSessionFromServices({\n\t\t\tservices,\n\t\t\tsessionManager,\n\t\t\tsessionStartEvent,\n\t\t\tmodel: sessionOptions.model,\n\t\t\tthinkingLevel: sessionOptions.thinkingLevel,\n\t\t\tscopedModels: sessionOptions.scopedModels,\n\t\t\ttools: sessionOptions.tools,\n\t\t\texcludeTools: sessionOptions.excludeTools,\n\t\t\tnoTools: sessionOptions.noTools,\n\t\t\tcustomTools: sessionOptions.customTools,\n\t\t\tpermissionGate: {\n\t\t\t\tstore: permissionStore,\n\t\t\t\tflagMode: parsed.permissionMode,\n\t\t\t\tgetMode: async () =>\n\t\t\t\t\tresolveEffectiveMode(parsed.permissionMode, (await permissionStore.snapshot()).modesBySource),\n\t\t\t},\n\t\t});\n\t\tconst unclassifiedToolNames = created.session\n\t\t\t.getAllTools()\n\t\t\t.filter((tool) => tool.unclassified)\n\t\t\t.map((tool) => tool.name);\n\t\tif (unclassifiedToolNames.length > 0) {\n\t\t\tdiagnostics.push({\n\t\t\t\ttype: \"warning\",\n\t\t\t\tmessage: `Unclassified tool contracts (approval required): ${unclassifiedToolNames.join(\", \")}`,\n\t\t\t});\n\t\t}\n\t\tconst cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;\n\t\tif (created.session.model && cliThinkingOverride) {\n\t\t\tcreated.session.setThinkingLevel(created.session.thinkingLevel);\n\t\t}\n\n\t\treturn {\n\t\t\t...created,\n\t\t\tservices,\n\t\t\tdiagnostics,\n\t\t};\n\t};\n\ttime(\"createRuntime\");\n\tconst runtime = await createAgentSessionRuntime(createRuntime, {\n\t\tcwd: sessionManager.getCwd(),\n\t\tagentDir,\n\t\tsessionManager,\n\t});\n\ttime(\"createAgentSessionRuntime\");\n\tconst { services, session, modelFallbackMessage } = runtime;\n\tconst { settingsManager, modelRuntime, resourceLoader } = services;\n\tapplyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy);\n\tconfigureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());\n\n\tif (parsed.help) {\n\t\tconst extensionFlags = resourceLoader\n\t\t\t.getExtensions()\n\t\t\t.extensions.flatMap((extension) => Array.from(extension.flags.values()));\n\t\tprintHelp(extensionFlags);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.listModels !== undefined) {\n\t\tconst searchPattern = typeof parsed.listModels === \"string\" ? parsed.listModels : undefined;\n\t\tawait listModels(modelRuntime, searchPattern, AbortSignal.timeout(15_000));\n\t\tprocess.exit(0);\n\t}\n\n\t// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC\n\tlet stdinContent: string | undefined;\n\tif (appMode !== \"rpc\") {\n\t\tstdinContent = await readPipedStdin();\n\t\tif (stdinContent !== undefined && appMode === \"interactive\") {\n\t\t\tappMode = \"print\";\n\t\t}\n\t}\n\ttime(\"readPipedStdin\");\n\n\tconst { initialMessage, initialImages } = await prepareInitialMessage(\n\t\tparsed,\n\t\tsettingsManager.getImageAutoResize(),\n\t\tstdinContent,\n\t);\n\ttime(\"prepareInitialMessage\");\n\tinitTheme(settingsManager.getTheme(), appMode === \"interactive\");\n\ttime(\"initTheme\");\n\n\t// Show deprecation warnings in interactive mode\n\tif (appMode === \"interactive\" && deprecationWarnings.length > 0) {\n\t\tawait showDeprecationWarnings(deprecationWarnings);\n\t}\n\n\ttime(\"resolveModelScope\");\n\treportDiagnostics(runtime.diagnostics);\n\tif (runtime.diagnostics.some((diagnostic) => diagnostic.type === \"error\")) {\n\t\tif (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes(\"Failed to load extension\"))) {\n\t\t\tconsole.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));\n\t\t}\n\t\tprocess.exit(1);\n\t}\n\ttime(\"createAgentSession\");\n\n\tif (appMode !== \"interactive\" && !session.model) {\n\t\tconsole.error(chalk.red(formatNoModelsAvailableMessage()));\n\t\tprocess.exit(1);\n\t}\n\n\tconst startupBenchmark = isTruthyEnvFlag(getApexEnvironment(\"APEX_CODE_STARTUP_BENCHMARK\"));\n\tif (startupBenchmark && appMode !== \"interactive\") {\n\t\tconsole.error(chalk.red(\"Error: APEX_CODE_STARTUP_BENCHMARK only supports interactive mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\t// RPC refreshes catalogs here in the background; interactive mode starts its refresh after TUI initialization.\n\tif (!offlineMode && appMode === \"rpc\") {\n\t\tconst controller = new AbortController();\n\t\tconst timeout = setTimeout(() => controller.abort(), 15_000);\n\t\tvoid modelRuntime\n\t\t\t.refresh({ signal: controller.signal })\n\t\t\t.catch(() => {})\n\t\t\t.finally(() => clearTimeout(timeout));\n\t}\n\n\tif (appMode === \"rpc\") {\n\t\tprintTimings();\n\t\tawait runRpcMode(runtime);\n\t} else if (appMode === \"interactive\") {\n\t\tconst interactiveMode = new InteractiveMode(runtime, {\n\t\t\tmigratedProviders,\n\t\t\tmodelFallbackMessage,\n\t\t\tautoTrustOnReloadCwd,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\tinitialMessages: parsed.messages,\n\t\t\tverbose: parsed.verbose,\n\t\t\ttuiMode: parsed.tuiMode,\n\t\t});\n\t\tif (startupBenchmark) {\n\t\t\tawait interactiveMode.init();\n\t\t\ttime(\"interactiveMode.init\");\n\t\t\t// Give the TUI's stdin handler a brief chance to consume terminal query replies\n\t\t\t// (Kitty keyboard protocol, device attributes, cell size) before restoring the terminal.\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 150));\n\t\t\tinteractiveMode.stop();\n\t\t\tstopThemeWatcher();\n\t\t\tprintTimings();\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tprintTimings();\n\t\tawait interactiveMode.run();\n\t} else {\n\t\tprintTimings();\n\t\tconst exitCode = await runPrintMode(runtime, {\n\t\t\tmode: toPrintOutputMode(appMode),\n\t\t\tmessages: parsed.messages,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t});\n\t\tstopThemeWatcher();\n\t\trestoreStdout();\n\t\tif (exitCode !== 0) {\n\t\t\tprocess.exitCode = exitCode;\n\t\t}\n\t\treturn;\n\t}\n}\n"]}
package/dist/main.js CHANGED
@@ -690,6 +690,7 @@ export async function main(args, options) {
690
690
  customTools: sessionOptions.customTools,
691
691
  permissionGate: {
692
692
  store: permissionStore,
693
+ flagMode: parsed.permissionMode,
693
694
  getMode: async () => resolveEffectiveMode(parsed.permissionMode, (await permissionStore.snapshot()).modesBySource),
694
695
  },
695
696
  });