agent-sh 0.12.27 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +13 -2
  2. package/dist/agent/agent-loop.d.ts +3 -5
  3. package/dist/agent/agent-loop.js +42 -98
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +16 -0
  6. package/dist/agent/history-file.d.ts +6 -0
  7. package/dist/agent/history-file.js +1 -1
  8. package/dist/agent/host-types.d.ts +125 -0
  9. package/dist/agent/index.d.ts +12 -4
  10. package/dist/agent/index.js +358 -6
  11. package/dist/agent/nuclear-form.d.ts +7 -0
  12. package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
  13. package/dist/{extensions → agent}/providers/deepseek.js +5 -4
  14. package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
  15. package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
  16. package/dist/{extensions → agent}/providers/openai.js +3 -2
  17. package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
  18. package/dist/{extensions → agent}/providers/openrouter.js +4 -3
  19. package/dist/agent/skills.js +51 -7
  20. package/dist/agent/subagent.d.ts +1 -1
  21. package/dist/agent/system-prompt.js +14 -17
  22. package/dist/agent/tool-protocol.d.ts +1 -1
  23. package/dist/agent/tool-protocol.js +5 -3
  24. package/dist/agent/tool-registry.d.ts +9 -4
  25. package/dist/agent/tool-registry.js +27 -4
  26. package/dist/agent/tools/bash.d.ts +1 -1
  27. package/dist/agent/tools/bash.js +3 -2
  28. package/dist/agent/tools/edit-file.js +0 -1
  29. package/dist/agent/tools/glob.js +1 -1
  30. package/dist/agent/tools/grep.js +1 -1
  31. package/dist/agent/tools/pwsh.d.ts +1 -1
  32. package/dist/agent/tools/pwsh.js +1 -2
  33. package/dist/agent/tools/read-file.js +7 -4
  34. package/dist/agent/tools/write-file.js +0 -1
  35. package/dist/agent/types.d.ts +17 -2
  36. package/dist/cli/auth/cli.d.ts +1 -0
  37. package/dist/cli/auth/cli.js +216 -0
  38. package/dist/cli/auth/keys.d.ts +31 -0
  39. package/dist/cli/auth/keys.js +102 -0
  40. package/dist/{index.js → cli/index.js} +29 -32
  41. package/dist/{init.js → cli/init.js} +1 -1
  42. package/dist/{install.js → cli/install.js} +31 -2
  43. package/dist/cli/subcommands.d.ts +1 -0
  44. package/dist/cli/subcommands.js +17 -0
  45. package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
  46. package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
  47. package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
  48. package/dist/{core.d.ts → core/index.d.ts} +18 -15
  49. package/dist/{core.js → core/index.js} +18 -92
  50. package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
  51. package/dist/{settings.js → core/settings.js} +1 -0
  52. package/dist/core/types.d.ts +49 -0
  53. package/dist/core/types.js +1 -0
  54. package/dist/extensions/file-autocomplete.d.ts +1 -1
  55. package/dist/extensions/index.d.ts +7 -14
  56. package/dist/extensions/index.js +2 -19
  57. package/dist/extensions/slash-commands.d.ts +1 -1
  58. package/dist/extensions/slash-commands.js +7 -2
  59. package/dist/shell/host-types.d.ts +114 -0
  60. package/dist/shell/host-types.js +1 -0
  61. package/dist/shell/index.d.ts +8 -7
  62. package/dist/shell/index.js +58 -9
  63. package/dist/shell/input-handler.d.ts +7 -1
  64. package/dist/shell/input-handler.js +5 -2
  65. package/dist/shell/output-parser.d.ts +1 -1
  66. package/dist/{extensions → shell}/shell-context.d.ts +1 -1
  67. package/dist/{extensions → shell}/shell-context.js +18 -12
  68. package/dist/shell/shell.d.ts +6 -4
  69. package/dist/shell/shell.js +33 -109
  70. package/dist/shell/strategies/bash.d.ts +2 -0
  71. package/dist/shell/strategies/bash.js +68 -0
  72. package/dist/shell/strategies/fish.d.ts +2 -0
  73. package/dist/shell/strategies/fish.js +65 -0
  74. package/dist/shell/strategies/index.d.ts +13 -0
  75. package/dist/shell/strategies/index.js +17 -0
  76. package/dist/shell/strategies/types.d.ts +50 -0
  77. package/dist/shell/strategies/types.js +9 -0
  78. package/dist/shell/strategies/zsh.d.ts +2 -0
  79. package/dist/shell/strategies/zsh.js +72 -0
  80. package/dist/shell/tui-input-view.js +14 -3
  81. package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
  82. package/dist/{extensions → shell}/tui-renderer.js +27 -55
  83. package/dist/utils/box-frame.d.ts +4 -0
  84. package/dist/utils/box-frame.js +17 -6
  85. package/dist/utils/compositor.d.ts +1 -1
  86. package/dist/utils/compositor.js +2 -1
  87. package/dist/{executor.js → utils/executor.js} +1 -1
  88. package/dist/utils/floating-panel.d.ts +1 -1
  89. package/dist/utils/floating-panel.js +9 -4
  90. package/dist/utils/llm-client.d.ts +16 -26
  91. package/dist/utils/llm-client.js +15 -26
  92. package/dist/utils/llm-facade.d.ts +7 -3
  93. package/dist/utils/stream-transform.d.ts +1 -1
  94. package/dist/utils/terminal-buffer.d.ts +1 -1
  95. package/dist/utils/tool-display.js +4 -0
  96. package/dist/utils/tool-interactive.d.ts +1 -1
  97. package/dist/utils/tty.d.ts +7 -0
  98. package/dist/utils/tty.js +15 -0
  99. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  100. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  101. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  102. package/examples/extensions/ashi/README.md +250 -0
  103. package/examples/extensions/ashi/package.json +60 -0
  104. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  105. package/examples/extensions/ashi/src/capture.ts +34 -0
  106. package/examples/extensions/ashi/src/cli.ts +176 -0
  107. package/examples/extensions/ashi/src/commands.ts +82 -0
  108. package/examples/extensions/ashi/src/compaction.ts +157 -0
  109. package/examples/extensions/ashi/src/components.ts +327 -0
  110. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  111. package/examples/extensions/ashi/src/display-config.ts +62 -0
  112. package/examples/extensions/ashi/src/frontend.ts +735 -0
  113. package/examples/extensions/ashi/src/hooks.ts +136 -0
  114. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  115. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  116. package/examples/extensions/ashi/src/session-store.ts +264 -0
  117. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  118. package/examples/extensions/ashi/src/theme.ts +151 -0
  119. package/examples/extensions/ashi/tsconfig.json +14 -0
  120. package/examples/extensions/emacs-buffer.ts +1 -1
  121. package/examples/extensions/interactive-prompts.ts +114 -69
  122. package/examples/extensions/latex-images.ts +3 -3
  123. package/examples/extensions/opencode-bridge/index.ts +1 -1
  124. package/examples/extensions/overlay-agent.ts +7 -5
  125. package/examples/extensions/peer-mesh.ts +1 -1
  126. package/examples/extensions/pi-bridge/index.ts +0 -1
  127. package/examples/extensions/questionnaire.ts +2 -1
  128. package/examples/extensions/rtk-proxy.ts +3 -3
  129. package/examples/extensions/solarized-theme.ts +3 -3
  130. package/examples/extensions/subagents.ts +6 -6
  131. package/examples/extensions/terminal-buffer.ts +1 -1
  132. package/examples/extensions/tmux-pane.ts +6 -4
  133. package/examples/extensions/tunnel-vision.ts +5 -5
  134. package/examples/extensions/user-shell.ts +1 -1
  135. package/examples/extensions/web-access.ts +5 -5
  136. package/package.json +38 -22
  137. package/dist/extensions/agent-backend.d.ts +0 -14
  138. package/dist/extensions/agent-backend.js +0 -307
  139. package/dist/types.d.ts +0 -227
  140. /package/dist/{types.js → agent/host-types.js} +0 -0
  141. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  142. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  143. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  144. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  145. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  146. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
@@ -1,15 +1,20 @@
1
- import type { ToolDefinition } from "./types.js";
1
+ import type { ToolDefinition, ToolResult } from "./types.js";
2
2
  import type { ChatCompletionTool } from "../utils/llm-client.js";
3
+ import type { HandlerFunctions } from "../utils/handler-registry.js";
3
4
  /**
4
- * Registry for agent tools. Holds tool definitions and converts them
5
- * to OpenAI-compatible function schemas for API calls.
5
+ * Registry for agent tools. Execution is routed through the named-handler
6
+ * registry under `tool:<name>` so extensions can `advise` a tool without
7
+ * owning it; duplicate `register` calls throw rather than silently evict.
6
8
  */
7
9
  export declare class ToolRegistry {
10
+ private handlers;
8
11
  private tools;
12
+ constructor(handlers: HandlerFunctions);
9
13
  register(tool: ToolDefinition): void;
10
14
  unregister(name: string): void;
11
15
  get(name: string): ToolDefinition | undefined;
12
16
  all(): ToolDefinition[];
13
- /** Convert to OpenAI-compatible tool schemas for API calls. */
17
+ allView(): ToolDefinition[];
18
+ call(name: string, ...args: Parameters<ToolDefinition["execute"]>): Promise<ToolResult>;
14
19
  toAPITools(): ChatCompletionTool[];
15
20
  }
@@ -1,12 +1,25 @@
1
1
  import { registerReadOnlyTool, unregisterReadOnlyTool } from "./nuclear-form.js";
2
2
  /**
3
- * Registry for agent tools. Holds tool definitions and converts them
4
- * to OpenAI-compatible function schemas for API calls.
3
+ * Registry for agent tools. Execution is routed through the named-handler
4
+ * registry under `tool:<name>` so extensions can `advise` a tool without
5
+ * owning it; duplicate `register` calls throw rather than silently evict.
5
6
  */
6
7
  export class ToolRegistry {
8
+ handlers;
7
9
  tools = new Map();
10
+ constructor(handlers) {
11
+ this.handlers = handlers;
12
+ }
8
13
  register(tool) {
14
+ if (this.tools.has(tool.name)) {
15
+ throw new Error(`Tool "${tool.name}" already registered. Use ctx.agent.adviseTool() to wrap it.`);
16
+ }
9
17
  this.tools.set(tool.name, tool);
18
+ this.handlers.define(`tool:${tool.name}`, tool.execute.bind(tool));
19
+ this.handlers.define(`tool:${tool.name}:schema`, () => ({
20
+ description: tool.description,
21
+ parameters: tool.input_schema,
22
+ }));
10
23
  if (tool.readOnly)
11
24
  registerReadOnlyTool(tool.name);
12
25
  else
@@ -15,6 +28,8 @@ export class ToolRegistry {
15
28
  unregister(name) {
16
29
  this.tools.delete(name);
17
30
  unregisterReadOnlyTool(name);
31
+ // Handler entry intentionally retained so external advisors survive a
32
+ // reload of the tool's owner and reattach when the name re-registers.
18
33
  }
19
34
  get(name) {
20
35
  return this.tools.get(name);
@@ -22,9 +37,17 @@ export class ToolRegistry {
22
37
  all() {
23
38
  return Array.from(this.tools.values());
24
39
  }
25
- /** Convert to OpenAI-compatible tool schemas for API calls. */
40
+ allView() {
41
+ return this.all().map((t) => {
42
+ const view = this.handlers.call(`tool:${t.name}:schema`);
43
+ return { ...t, description: view.description, input_schema: view.parameters };
44
+ });
45
+ }
46
+ call(name, ...args) {
47
+ return this.handlers.call(`tool:${name}`, ...args);
48
+ }
26
49
  toAPITools() {
27
- return this.all().map((t) => ({
50
+ return this.allView().map((t) => ({
28
51
  type: "function",
29
52
  function: {
30
53
  name: t.name,
@@ -1,4 +1,4 @@
1
- import type { EventBus } from "../../event-bus.js";
1
+ import type { EventBus } from "../../core/event-bus.js";
2
2
  import type { ToolDefinition } from "../types.js";
3
3
  export declare function createBashTool(opts: {
4
4
  getCwd: () => string;
@@ -1,10 +1,12 @@
1
- import { executeCommand, killSession } from "../../executor.js";
1
+ import { executeCommand, killSession } from "../../utils/executor.js";
2
2
  export function createBashTool(opts) {
3
3
  return {
4
4
  name: "bash",
5
5
  description: "Execute a bash command in an isolated subprocess. Output is captured and returned. " +
6
6
  "Does not affect the user's shell state. " +
7
7
  "cwd is set to the working directory from the shell context. " +
8
+ "Keep commands focused; avoid long-running or blocking processes. " +
9
+ "Always check the returned exit code before treating output as success. " +
8
10
  "Do NOT use bash for file searching — use grep/glob instead. " +
9
11
  "Do NOT use bash for reading files — use read_file instead.",
10
12
  input_schema: {
@@ -27,7 +29,6 @@ export function createBashTool(opts) {
27
29
  },
28
30
  showOutput: true,
29
31
  modifiesFiles: true,
30
- requiresPermission: true,
31
32
  getDisplayInfo: (args) => ({
32
33
  kind: "execute",
33
34
  icon: "▶",
@@ -63,7 +63,6 @@ export function createEditFileTool(getCwd) {
63
63
  },
64
64
  showOutput: true,
65
65
  modifiesFiles: true,
66
- requiresPermission: true,
67
66
  getDisplayInfo: (args) => ({
68
67
  kind: "write",
69
68
  icon: "✎",
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { executeArgv } from "../../executor.js";
3
+ import { executeArgv } from "../../utils/executor.js";
4
4
  import { resolveRgPath } from "../../utils/ripgrep-path.js";
5
5
  import { expandHome } from "./expand-home.js";
6
6
  export function createGlobTool(getCwd) {
@@ -1,4 +1,4 @@
1
- import { executeArgv } from "../../executor.js";
1
+ import { executeArgv } from "../../utils/executor.js";
2
2
  import { resolveRgPath } from "../../utils/ripgrep-path.js";
3
3
  import { expandHome } from "./expand-home.js";
4
4
  export function createGrepTool(getCwd) {
@@ -1,4 +1,4 @@
1
- import type { EventBus } from "../../event-bus.js";
1
+ import type { EventBus } from "../../core/event-bus.js";
2
2
  import type { ToolDefinition } from "../types.js";
3
3
  export declare function createPwshTool(opts: {
4
4
  getCwd: () => string;
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { executeArgv, killSession } from "../../executor.js";
2
+ import { executeArgv, killSession } from "../../utils/executor.js";
3
3
  let cachedPwshPath;
4
4
  /** Resolve a usable PowerShell binary, or null if none is on PATH.
5
5
  * Prefers PowerShell 7+ (`pwsh`), falls back to Windows PowerShell (`powershell`). */
@@ -55,7 +55,6 @@ export function createPwshTool(opts) {
55
55
  },
56
56
  showOutput: true,
57
57
  modifiesFiles: true,
58
- requiresPermission: true,
59
58
  getDisplayInfo: () => ({
60
59
  kind: "execute",
61
60
  icon: "▶",
@@ -46,8 +46,10 @@ export function createReadFileTool(getCwd, cache) {
46
46
  const reqLimit = args.limit;
47
47
  try {
48
48
  const stat = await fs.stat(absPath);
49
- // Deduplication: if the file hasn't changed and same range, return stub
50
- if (cache) {
49
+ // Deduplication: if the file hasn't changed and same range, return stub.
50
+ // bypass_cache lets in-process callers consume content as a value
51
+ // rather than a tool_result, where the dedup stub would be meaningless.
52
+ if (cache && !args.bypass_cache) {
51
53
  const prev = cache.get(absPath);
52
54
  if (prev &&
53
55
  prev.mtimeMs === stat.mtimeMs &&
@@ -83,8 +85,9 @@ export function createReadFileTool(getCwd, cache) {
83
85
  const suffix = truncated
84
86
  ? `\n[${lines.length - end} more lines, use offset=${end + 1} to continue]`
85
87
  : "";
86
- // Update cache on successful read
87
- if (cache) {
88
+ // Skip cache write for in-process callers — they shouldn't poison
89
+ // the LLM-facing dedup state.
90
+ if (cache && !args.bypass_cache) {
88
91
  cache.set(absPath, {
89
92
  mtimeMs: stat.mtimeMs,
90
93
  offset: reqOffset,
@@ -23,7 +23,6 @@ export function createWriteFileTool(getCwd) {
23
23
  },
24
24
  showOutput: true,
25
25
  modifiesFiles: true,
26
- requiresPermission: true,
27
26
  getDisplayInfo: (args) => ({
28
27
  kind: "write",
29
28
  icon: "✎",
@@ -47,6 +47,9 @@ export interface ToolDisplayInfo {
47
47
  /** Custom icon character for TUI display (e.g., "◆", "⌕"). When set, the TUI shows
48
48
  * icon + detail only. When absent, the tool name is shown alongside the detail. */
49
49
  icon?: string;
50
+ /** highlight.js-style language identifier ("scheme", "python", …) for
51
+ * renderers that syntax-highlight tool source. Omit for plain text. */
52
+ sourceLanguage?: string;
50
53
  }
51
54
  /** Interactive UI session — imperative control over rendering + input. */
52
55
  export interface InteractiveSession<T> {
@@ -70,6 +73,16 @@ export interface ToolExecutionContext {
70
73
  /** Aborted on Ctrl-C — tools with subprocess work should listen and clean up. */
71
74
  signal?: AbortSignal;
72
75
  }
76
+ /** LLM-facing view of a tool — what `adviseToolSchema` advisors return. */
77
+ export interface ToolSchemaView {
78
+ description: string;
79
+ parameters: Record<string, unknown>;
80
+ }
81
+ /** LLM-facing view of a skill — what `adviseSkill` advisors return. */
82
+ export interface SkillView {
83
+ description: string;
84
+ filePath: string;
85
+ }
73
86
  export interface ToolDefinition {
74
87
  name: string;
75
88
  /** Short label for TUI display (e.g. "search" instead of "ads_search"). Defaults to name. */
@@ -84,8 +97,6 @@ export interface ToolDefinition {
84
97
  /** Results are re-fetchable; nuclear compaction drops the tool_result
85
98
  * body on eviction (like the builtin read_file/grep/ls). Default: false. */
86
99
  readOnly?: boolean;
87
- /** Whether to gate execution via permission:request (default: false). */
88
- requiresPermission?: boolean;
89
100
  /** Derive display metadata (icon kind, file paths) for the TUI. */
90
101
  getDisplayInfo?: (args: Record<string, unknown>) => ToolDisplayInfo;
91
102
  /**
@@ -101,4 +112,8 @@ export interface ToolDefinition {
101
112
  * Extensions can further override via bus.onPipe("agent:tool-completed", ...).
102
113
  */
103
114
  formatResult?: (args: Record<string, unknown>, result: ToolResult) => ToolResultDisplay;
115
+ /** Override the agent-loop's per-tool-result truncation cap (default 16 KB).
116
+ * Use for tools that bundle multiple operations and legitimately produce
117
+ * larger output (interpreter substrates etc.). */
118
+ maxResultBytes?: number;
104
119
  }
@@ -0,0 +1 @@
1
+ export declare function runAuth(args: string[]): Promise<void>;
@@ -0,0 +1,216 @@
1
+ import * as readline from "node:readline";
2
+ import { palette as p } from "../../utils/palette.js";
3
+ import { KNOWN_PROVIDERS, KEYS_PATH, loadKeysFile, saveKeysFile, resolveApiKey, listAllProviders, findProvider as findProviderById, } from "./keys.js";
4
+ export async function runAuth(args) {
5
+ const sub = args[0];
6
+ if (!sub || sub === "--help" || sub === "-h") {
7
+ printHelp();
8
+ return;
9
+ }
10
+ if (sub === "login") {
11
+ await runLogin(args[1]);
12
+ return;
13
+ }
14
+ if (sub === "logout") {
15
+ runLogout(args[1]);
16
+ return;
17
+ }
18
+ if (sub === "list" || sub === "ls" || sub === "status") {
19
+ runList();
20
+ return;
21
+ }
22
+ console.error(`agent-sh auth: unknown subcommand "${sub}"`);
23
+ printHelp();
24
+ process.exit(1);
25
+ }
26
+ function printHelp() {
27
+ const builtins = KNOWN_PROVIDERS.map((p) => p.id).join(" | ");
28
+ console.log(`agent-sh auth — manage API keys for providers\n\n` +
29
+ `Usage:\n` +
30
+ ` agent-sh auth login [provider] Store an API key (prompts if omitted)\n` +
31
+ ` agent-sh auth logout <provider> Remove a stored key\n` +
32
+ ` agent-sh auth list Show configured providers and key sources\n\n` +
33
+ `Built-in providers: ${builtins}\n` +
34
+ `Custom providers declared in settings.json are also accepted by id.\n\n` +
35
+ `Keys are stored in ${KEYS_PATH} (chmod 0600).\n` +
36
+ `Resolution order: settings.json > keys.json > env var.\n`);
37
+ }
38
+ async function runLogin(providerArg) {
39
+ let provider;
40
+ if (providerArg) {
41
+ const id = providerArg.toLowerCase();
42
+ if (!/^[a-z0-9][a-z0-9_\-./:]*$/.test(id)) {
43
+ console.error(`agent-sh auth: invalid provider id "${providerArg}".`);
44
+ process.exit(1);
45
+ }
46
+ provider = findProviderById(id);
47
+ if (!provider) {
48
+ console.error(`${p.warning}Note:${p.reset} no built-in or settings.json provider named "${id}". ` +
49
+ `Storing the key anyway — it will resolve once an extension or settings.json declares the provider.`);
50
+ provider = { id, label: id, unattached: true };
51
+ }
52
+ }
53
+ else {
54
+ provider = await pickProvider();
55
+ }
56
+ if (!provider)
57
+ process.exit(1);
58
+ const key = await promptSecret(`Enter ${provider.label} API key: `);
59
+ const trimmed = key.trim();
60
+ if (!trimmed) {
61
+ console.error("agent-sh auth: empty key, nothing saved.");
62
+ process.exit(1);
63
+ }
64
+ if (/\s/.test(trimmed)) {
65
+ console.error("agent-sh auth: key contains whitespace; aborting.");
66
+ process.exit(1);
67
+ }
68
+ const keys = { ...loadKeysFile() };
69
+ keys[provider.id] = trimmed;
70
+ saveKeysFile(keys);
71
+ const resolved = resolveApiKey(provider.id);
72
+ console.log(`${p.success}✓${p.reset} Saved ${provider.label} key to ${KEYS_PATH}`);
73
+ if (resolved.source !== "keys-file") {
74
+ console.log(`${p.warning}Note:${p.reset} an existing ${sourceLabel(resolved.source, provider)} entry ` +
75
+ `takes precedence; the stored key is shadowed until you remove it.`);
76
+ }
77
+ }
78
+ function runLogout(providerArg) {
79
+ if (!providerArg) {
80
+ console.error("Usage: agent-sh auth logout <provider>");
81
+ process.exit(1);
82
+ }
83
+ const id = providerArg.toLowerCase();
84
+ const keys = { ...loadKeysFile() };
85
+ if (!(id in keys)) {
86
+ console.log(`agent-sh auth: no stored key for ${id}.`);
87
+ return;
88
+ }
89
+ delete keys[id];
90
+ saveKeysFile(keys);
91
+ console.log(`${p.success}✓${p.reset} Removed ${id} key from ${KEYS_PATH}`);
92
+ }
93
+ function runList() {
94
+ const providers = listAllProviders();
95
+ console.log("Provider key status:\n");
96
+ const idWidth = Math.max(...providers.map((p) => p.id.length));
97
+ for (const info of providers) {
98
+ const resolved = resolveApiKey(info.id);
99
+ const padded = info.id.padEnd(idWidth);
100
+ const marker = info.custom
101
+ ? ` ${p.dim}custom${p.reset}`
102
+ : info.unattached
103
+ ? ` ${p.dim}unattached${p.reset}`
104
+ : "";
105
+ if (resolved.key) {
106
+ console.log(` ${p.success}●${p.reset} ${padded} ${p.dim}(${sourceLabel(resolved.source, info)})${p.reset}${marker}`);
107
+ }
108
+ else {
109
+ console.log(` ${p.muted}○${p.reset} ${padded} ${p.dim}(not configured)${p.reset}${marker}`);
110
+ }
111
+ }
112
+ const example = providers[0].id;
113
+ console.log(`\n${p.muted}Login with:${p.reset} agent-sh auth login <id> ${p.dim}(e.g. agent-sh auth login ${example})${p.reset}`);
114
+ }
115
+ async function pickProvider() {
116
+ if (!process.stdin.isTTY) {
117
+ console.error("agent-sh auth: no provider specified and stdin is not a TTY.");
118
+ return null;
119
+ }
120
+ const providers = listAllProviders();
121
+ console.log("Select a provider:");
122
+ providers.forEach((info, i) => {
123
+ const resolved = resolveApiKey(info.id);
124
+ const tag = resolved.key
125
+ ? `${p.dim}(currently from ${sourceLabel(resolved.source, info)})${p.reset}`
126
+ : `${p.dim}(not configured)${p.reset}`;
127
+ const labelStr = info.custom
128
+ ? `${p.dim}custom${p.reset}`
129
+ : info.unattached
130
+ ? `${p.dim}unattached${p.reset}`
131
+ : `${p.dim}${info.label}${p.reset}`;
132
+ console.log(` ${i + 1}) ${info.id.padEnd(12)} ${labelStr} ${tag}`);
133
+ });
134
+ const answer = await promptLine("Choice [1]: ");
135
+ const idx = answer.trim() === "" ? 0 : Number(answer.trim()) - 1;
136
+ if (!Number.isInteger(idx) || idx < 0 || idx >= providers.length) {
137
+ console.error("agent-sh auth: invalid selection.");
138
+ return null;
139
+ }
140
+ return providers[idx];
141
+ }
142
+ function promptLine(question) {
143
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
144
+ return new Promise((resolve) => {
145
+ rl.question(question, (answer) => {
146
+ rl.close();
147
+ resolve(answer);
148
+ });
149
+ });
150
+ }
151
+ function promptSecret(question) {
152
+ return new Promise((resolve) => {
153
+ if (!process.stdin.isTTY) {
154
+ let buf = "";
155
+ process.stdin.setEncoding("utf-8");
156
+ process.stdin.on("data", (chunk) => {
157
+ buf += chunk;
158
+ const nl = buf.indexOf("\n");
159
+ if (nl >= 0) {
160
+ process.stdin.pause();
161
+ resolve(buf.slice(0, nl).replace(/\r$/, ""));
162
+ }
163
+ });
164
+ return;
165
+ }
166
+ const stdin = process.stdin;
167
+ const stdout = process.stdout;
168
+ const wasRaw = stdin.isRaw;
169
+ stdout.write(question);
170
+ stdin.resume();
171
+ stdin.setRawMode(true);
172
+ stdin.setEncoding("utf-8");
173
+ let buf = "";
174
+ const finish = (value) => {
175
+ stdout.write("\n");
176
+ stdin.removeListener("data", onData);
177
+ stdin.setRawMode(wasRaw);
178
+ stdin.pause();
179
+ if (value === null)
180
+ process.exit(130);
181
+ resolve(value);
182
+ };
183
+ const onData = (ch) => {
184
+ for (const c of ch) {
185
+ if (c === "\n" || c === "\r" || c === "\x04") {
186
+ finish(buf);
187
+ return;
188
+ }
189
+ if (c === "\x03") {
190
+ finish(null);
191
+ return;
192
+ }
193
+ if (c === "\x7f" || c === "\b") {
194
+ if (buf.length > 0) {
195
+ buf = buf.slice(0, -1);
196
+ stdout.write("\b \b");
197
+ }
198
+ continue;
199
+ }
200
+ if (c < " ")
201
+ continue;
202
+ buf += c;
203
+ stdout.write("*");
204
+ }
205
+ };
206
+ stdin.on("data", onData);
207
+ });
208
+ }
209
+ function sourceLabel(source, info) {
210
+ switch (source) {
211
+ case "settings": return "settings.json";
212
+ case "keys-file": return "keys.json";
213
+ case "env": return info ? `$${info.envVar}` : "env var";
214
+ default: return source;
215
+ }
216
+ }
@@ -0,0 +1,31 @@
1
+ export declare const KEYS_PATH: string;
2
+ export interface ProviderAuthInfo {
3
+ id: string;
4
+ label: string;
5
+ /** Conventional env var. Absent for user-declared providers. */
6
+ envVar?: string;
7
+ /** True for entries declared in settings.json (vs. a built-in). */
8
+ custom?: boolean;
9
+ /** True for ids only present in keys.json — likely owned by an extension
10
+ * that registers a provider at runtime. */
11
+ unattached?: boolean;
12
+ }
13
+ export declare const KNOWN_PROVIDERS: ProviderAuthInfo[];
14
+ /** Built-ins merged with settings-declared providers, plus any ids that only
15
+ * appear in keys.json (likely registered by an extension at runtime). */
16
+ export declare function listAllProviders(): ProviderAuthInfo[];
17
+ /** Resolve an id against known + settings entries only. Returns null for
18
+ * unattached or unknown ids — callers decide whether to accept them. */
19
+ export declare function findProvider(id: string): ProviderAuthInfo | null;
20
+ export type KeySource = "settings" | "keys-file" | "env" | "none";
21
+ export interface ResolvedKey {
22
+ key: string | null;
23
+ source: KeySource;
24
+ }
25
+ type KeysFile = Record<string, string>;
26
+ export declare function loadKeysFile(): KeysFile;
27
+ export declare function saveKeysFile(keys: KeysFile): void;
28
+ export declare function reloadKeysFile(): void;
29
+ export declare function resolveApiKey(providerId: string): ResolvedKey;
30
+ export declare function anyProviderConfigured(): boolean;
31
+ export {};
@@ -0,0 +1,102 @@
1
+ // Resolution order: settings.json → keys.json → env var.
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { CONFIG_DIR, getSettings, expandEnvVars } from "../../core/settings.js";
5
+ export const KEYS_PATH = path.join(CONFIG_DIR, "keys.json");
6
+ export const KNOWN_PROVIDERS = [
7
+ { id: "openai", label: "OpenAI", envVar: "OPENAI_API_KEY" },
8
+ { id: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY" },
9
+ { id: "deepseek", label: "DeepSeek", envVar: "DEEPSEEK_API_KEY" },
10
+ ];
11
+ /** Built-ins merged with settings-declared providers, plus any ids that only
12
+ * appear in keys.json (likely registered by an extension at runtime). */
13
+ export function listAllProviders() {
14
+ const out = [...KNOWN_PROVIDERS];
15
+ const seen = new Set(out.map((p) => p.id));
16
+ const settingsProviders = getSettings().providers ?? {};
17
+ for (const id of Object.keys(settingsProviders)) {
18
+ if (seen.has(id))
19
+ continue;
20
+ out.push({ id, label: id, custom: true });
21
+ seen.add(id);
22
+ }
23
+ for (const id of Object.keys(loadKeysFile())) {
24
+ if (seen.has(id))
25
+ continue;
26
+ out.push({ id, label: id, unattached: true });
27
+ seen.add(id);
28
+ }
29
+ return out;
30
+ }
31
+ /** Resolve an id against known + settings entries only. Returns null for
32
+ * unattached or unknown ids — callers decide whether to accept them. */
33
+ export function findProvider(id) {
34
+ const lower = id.toLowerCase();
35
+ const known = KNOWN_PROVIDERS.find((p) => p.id === lower);
36
+ if (known)
37
+ return known;
38
+ const settings = getSettings().providers ?? {};
39
+ if (lower in settings)
40
+ return { id: lower, label: lower, custom: true };
41
+ return null;
42
+ }
43
+ let cached = null;
44
+ export function loadKeysFile() {
45
+ if (cached)
46
+ return cached;
47
+ try {
48
+ const raw = fs.readFileSync(KEYS_PATH, "utf-8");
49
+ const parsed = JSON.parse(raw);
50
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
51
+ cached = parsed;
52
+ }
53
+ else {
54
+ cached = {};
55
+ }
56
+ }
57
+ catch (err) {
58
+ if (err instanceof SyntaxError) {
59
+ console.error(`[agent-sh] Warning: invalid JSON in ${KEYS_PATH}: ${err.message}`);
60
+ }
61
+ cached = {};
62
+ }
63
+ return cached;
64
+ }
65
+ export function saveKeysFile(keys) {
66
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
67
+ const tmp = `${KEYS_PATH}.tmp`;
68
+ fs.writeFileSync(tmp, JSON.stringify(keys, null, 2) + "\n", { mode: 0o600 });
69
+ fs.renameSync(tmp, KEYS_PATH);
70
+ try {
71
+ fs.chmodSync(KEYS_PATH, 0o600);
72
+ }
73
+ catch { /* best effort */ }
74
+ cached = { ...keys };
75
+ }
76
+ export function reloadKeysFile() {
77
+ cached = null;
78
+ }
79
+ export function resolveApiKey(providerId) {
80
+ const settingsKey = getSettings().providers?.[providerId]?.apiKey;
81
+ if (settingsKey) {
82
+ const expanded = expandEnvVars(settingsKey);
83
+ if (expanded)
84
+ return { key: expanded, source: "settings" };
85
+ }
86
+ const fileKey = loadKeysFile()[providerId];
87
+ if (fileKey)
88
+ return { key: fileKey, source: "keys-file" };
89
+ const info = KNOWN_PROVIDERS.find((p) => p.id === providerId);
90
+ if (info?.envVar) {
91
+ const envKey = process.env[info.envVar];
92
+ if (envKey)
93
+ return { key: envKey, source: "env" };
94
+ }
95
+ return { key: null, source: "none" };
96
+ }
97
+ export function anyProviderConfigured() {
98
+ // openai-compatible activates on OPENAI_BASE_URL alone (keyless local servers).
99
+ if (process.env.OPENAI_BASE_URL)
100
+ return true;
101
+ return listAllProviders().some((p) => resolveApiKey(p.id).key);
102
+ }