@inneranimalmedia/agentsam-sdk 2.5.0 → 2.6.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 (211) hide show
  1. package/AGENTSAM.md +55 -0
  2. package/README.md +12 -8
  3. package/bin/agentsam +2 -0
  4. package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
  5. package/docs/CLI_SHELL.md +163 -53
  6. package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
  7. package/docs/RELEASES.md +16 -7
  8. package/docs/SOURCE_ARCHITECTURE.md +58 -0
  9. package/docs/TEST_TIERS.md +26 -0
  10. package/migrations/runtime/0001_cli_runtime.sql +298 -0
  11. package/package.json +45 -12
  12. package/packages/agentsam-repository/README.md +15 -0
  13. package/packages/agentsam-repository/package.json +25 -0
  14. package/packages/agentsam-repository/src/contracts.js +113 -0
  15. package/packages/agentsam-repository/src/index.js +3 -0
  16. package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
  17. package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
  18. package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
  19. package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
  20. package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
  21. package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
  22. package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
  23. package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
  24. package/packages/connectors/cloudflare/package.json +10 -0
  25. package/packages/connectors/cloudflare/src/index.js +127 -0
  26. package/packages/connectors/cloudflare/src/owner.js +76 -0
  27. package/packages/connectors/cloudflare/src/routes.js +223 -0
  28. package/packages/connectors/cloudflare/src/vault.js +80 -0
  29. package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
  30. package/packages/identity/package.json +2 -2
  31. package/packages/identity/src/contracts/auth-config.js +18 -7
  32. package/packages/identity/tests/auth-config.test.mjs +9 -5
  33. package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
  34. package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
  35. package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
  36. package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
  37. package/protocol/README.md +1 -0
  38. package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
  39. package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
  40. package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
  41. package/protocol/capabilities/manifest.json +47 -0
  42. package/protocol/context/context-budget.schema.json +10 -15
  43. package/protocol/context/context-item.schema.json +4 -5
  44. package/protocol/context/resolved-context-pack.schema.json +19 -14
  45. package/protocol/models/README.md +373 -0
  46. package/protocol/models/model-inventory-v2.schema.json +212 -0
  47. package/protocol/repository/repository-contract.schema.json +24 -0
  48. package/protocol/repository/repository-dependency.schema.json +24 -0
  49. package/protocol/repository/repository-identity.schema.json +17 -0
  50. package/protocol/rpc/v1/common.proto +16 -0
  51. package/protocol/rpc/v1/errors.proto +35 -0
  52. package/protocol/rpc/v1/knowledge.proto +77 -0
  53. package/services/knowledge/package-lock.json +333 -0
  54. package/services/knowledge/package.json +5 -1
  55. package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
  56. package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
  57. package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
  58. package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
  59. package/skills/catalog.json +18 -0
  60. package/src/agent/capability-adapter.js +25 -13
  61. package/src/agent/index.js +1 -0
  62. package/src/agent/responses-runner.js +353 -0
  63. package/src/capabilities/repository-snapshot.js +3 -3
  64. package/src/cli.js +118 -31
  65. package/src/cloudflare/cpu-profile.js +115 -0
  66. package/src/cloudflare/index.js +14 -0
  67. package/src/cloudflare/wrangler.js +132 -0
  68. package/src/commands/account-auth.js +47 -0
  69. package/src/commands/cloudflare.js +58 -0
  70. package/src/commands/connections.js +93 -0
  71. package/src/commands/context-economics.js +129 -0
  72. package/src/commands/context.js +1 -1
  73. package/src/commands/db.js +20 -3
  74. package/src/commands/deploy.js +39 -3
  75. package/src/commands/env.js +90 -0
  76. package/src/commands/eval.js +63 -0
  77. package/src/commands/interactive.js +2 -5
  78. package/src/commands/knowledge.js +12 -4
  79. package/src/commands/merkle-persist.js +30 -11
  80. package/src/commands/merkle.js +1 -1
  81. package/src/commands/models.js +149 -46
  82. package/src/commands/ollama.js +26 -0
  83. package/src/commands/preferences.js +130 -61
  84. package/src/commands/resume.js +67 -0
  85. package/src/commands/security.js +5 -3
  86. package/src/commands/shell.js +568 -119
  87. package/src/commands/tunnel.js +2 -2
  88. package/src/commands/whoami.js +86 -0
  89. package/src/context/budget.js +68 -6
  90. package/src/context/index.js +3 -1
  91. package/src/context/rehydrate.js +35 -0
  92. package/src/context/resolve.js +44 -12
  93. package/src/errors/contract.js +236 -0
  94. package/src/errors/diagnostic.js +160 -0
  95. package/src/errors/index.js +23 -0
  96. package/src/eval/context.js +191 -0
  97. package/src/eval/index.js +1 -0
  98. package/src/index.js +68 -2
  99. package/src/knowledge/service/auth.js +13 -0
  100. package/src/knowledge/service/grpc-client.js +115 -0
  101. package/src/knowledge/service/grpc-codec.js +237 -0
  102. package/src/knowledge/service/grpc-server.js +83 -0
  103. package/src/knowledge/service/job-engine.js +248 -0
  104. package/src/knowledge/service/server.js +87 -135
  105. package/src/knowledge/source.js +1 -1
  106. package/src/lib/account-session.js +98 -0
  107. package/src/lib/agent-instructions.js +73 -0
  108. package/src/lib/auth.js +4 -0
  109. package/src/lib/cli-preferences.js +55 -24
  110. package/src/lib/deploy/git-guard.js +69 -0
  111. package/src/lib/deploy/health.js +57 -0
  112. package/src/lib/deploy/local-studio.js +283 -0
  113. package/src/lib/deploy/secret-scan.js +65 -0
  114. package/src/lib/deploy-receipt/index.js +2 -2
  115. package/src/lib/detect-context.js +2 -2
  116. package/src/lib/execution-approvals.js +59 -0
  117. package/src/lib/knowledge-docker.js +6 -3
  118. package/src/lib/local-sessions.js +148 -0
  119. package/src/lib/local-status.js +1 -1
  120. package/src/lib/project-config.js +1 -1
  121. package/src/lib/provider-credentials.js +183 -0
  122. package/src/lib/scaffold/templates/worker-api/index.js +101 -20
  123. package/src/lib/scaffold/wizards/worker-api.js +27 -11
  124. package/src/lib/slash-commands.js +23 -16
  125. package/src/local/migrations.js +93 -0
  126. package/src/local/runtime-store.js +141 -0
  127. package/src/local/sqlite.js +2 -0
  128. package/src/local-pty/server.js +113 -51
  129. package/src/models/catalog.js +135 -0
  130. package/src/models/discovery.js +292 -0
  131. package/src/models/index.js +7 -0
  132. package/src/providers/anthropic-messages.js +192 -0
  133. package/src/providers/cloudflare-chat.js +183 -0
  134. package/src/providers/factory.js +69 -0
  135. package/src/providers/gemini-generate-content.js +208 -0
  136. package/src/providers/index.js +10 -0
  137. package/src/providers/ollama-chat.js +148 -0
  138. package/src/providers/openai-responses.js +426 -0
  139. package/src/repository/index.js +14 -2
  140. package/src/rpc/generated/common_grpc_pb.js +1 -0
  141. package/src/rpc/generated/common_pb.js +536 -0
  142. package/src/rpc/generated/errors_grpc_pb.js +1 -0
  143. package/src/rpc/generated/errors_pb.js +482 -0
  144. package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
  145. package/src/rpc/generated/knowledge_pb.js +2168 -0
  146. package/src/rpc/generated/package.json +3 -0
  147. package/src/security/process.js +35 -9
  148. package/src/security/trust-boundary.js +2 -2
  149. package/src/telemetry/contracts.js +203 -0
  150. package/src/telemetry/events.js +51 -0
  151. package/src/telemetry/index.js +8 -0
  152. package/src/tools/hydrate.js +35 -0
  153. package/src/tools/index.js +1 -0
  154. package/src/ui/boot.js +15 -17
  155. package/src/ui/cli/activity.js +76 -0
  156. package/src/ui/cli/compaction.js +15 -0
  157. package/src/ui/cli/footer.js +39 -0
  158. package/src/ui/cli/help.js +192 -0
  159. package/src/ui/cli/plan.js +20 -0
  160. package/src/ui/cli/runtime-events.js +110 -0
  161. package/src/ui/cli/waiting.js +16 -0
  162. package/src/ui/merkle/render.js +1 -1
  163. package/test/account-session.test.mjs +36 -0
  164. package/test/cli/preferences-runtime.test.mjs +11 -0
  165. package/test/cli/runtime-ui.test.mjs +74 -0
  166. package/test/cli-preferences.test.mjs +26 -5
  167. package/test/cloudflare-connector.test.mjs +96 -0
  168. package/test/cloudflare-runtime.test.mjs +75 -0
  169. package/test/context.test.mjs +61 -12
  170. package/test/deploy-health-scan.test.mjs +67 -0
  171. package/test/error-diagnostics.test.mjs +115 -0
  172. package/test/eval-context.test.mjs +37 -0
  173. package/test/execution-approvals.test.mjs +27 -0
  174. package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
  175. package/test/integration/cli-help.test.mjs +37 -0
  176. package/test/integration/knowledge-rpc.test.mjs +112 -0
  177. package/test/integration/merkle-cli.test.mjs +61 -0
  178. package/test/integration/merkle-persistence-identity.test.mjs +48 -0
  179. package/test/integration/provider-env-cli.test.mjs +49 -0
  180. package/test/integration/provider-factory.test.mjs +197 -0
  181. package/test/integration/repository-company-graph.test.mjs +90 -0
  182. package/test/integration/runtime-migrations.test.mjs +82 -0
  183. package/test/knowledge-service.test.mjs +5 -0
  184. package/test/knowledge.test.mjs +16 -0
  185. package/test/live/terminal-transport.live.test.mjs +24 -0
  186. package/test/local-sessions.test.mjs +48 -0
  187. package/test/local-studio-deploy.test.mjs +83 -0
  188. package/test/model-catalog.test.mjs +43 -0
  189. package/test/models.test.mjs +127 -16
  190. package/test/npm10-lock.test.mjs +29 -0
  191. package/test/ollama.test.mjs +21 -0
  192. package/test/openai-responses.test.mjs +95 -0
  193. package/test/portable-context.test.mjs +1 -1
  194. package/test/provider-credentials.test.mjs +96 -0
  195. package/test/rehydrate.test.mjs +25 -0
  196. package/test/release-hygiene.test.mjs +13 -5
  197. package/test/responses-runner.test.mjs +150 -0
  198. package/test/shell.test.mjs +92 -23
  199. package/test/smoke.mjs +4 -1
  200. package/test/telemetry.test.mjs +79 -0
  201. package/test/terminal/local-pty.mock.test.mjs +151 -0
  202. package/test/tools-search.test.mjs +14 -1
  203. package/test/whoami-resume.test.mjs +56 -0
  204. /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
  205. /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
  206. /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
  207. /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
  208. /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
  209. /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
  210. /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
  211. /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
@@ -1,10 +1,12 @@
1
1
  import path from 'node:path';
2
- import { readSnapshot } from '../lib/merkle/snapshot.js';
2
+ import { readSnapshot } from '../../packages/agentsam-repository/src/merkle/snapshot.js';
3
+ import { readAccountSession } from '../lib/account-session.js';
4
+ import { getRepositoryId, portableRepositoryIdFromGit, tryReadProjectConfig } from '../lib/project-config.js';
3
5
  import {
4
6
  buildMerklePersistencePlan,
5
7
  persistMerkleSnapshotCloudflare,
6
8
  resolveWranglerMerklePersistence,
7
- } from '../lib/merkle/cloudflare-persistence.js';
9
+ } from '../../packages/agentsam-repository/src/merkle/cloudflare-persistence.js';
8
10
 
9
11
  function value(args, index, flag) {
10
12
  const next = args[index + 1];
@@ -17,8 +19,6 @@ export function printMerklePersistHelp() {
17
19
  agentsam merkle persist <snapshot.json> — publish a saved Merkle snapshot through host bindings
18
20
 
19
21
  --wrangler-config <file> Worker config containing WEBSITE_ASSETS and optionally DB
20
- --owner-user-id <id> Snapshot owner authority (or AGENTSAM_OWNER_USER_ID)
21
- --repo-id <id> Canonical repo id; inferred for GitHub/GitLab/Bitbucket when possible
22
22
  --capture-kind <kind> deploy|manual|agent|index (default manual)
23
23
  --connection-id <id> Execution provenance for non-deploy captures
24
24
  --runtime-lease-id <id> Alternative execution provenance for non-deploy captures
@@ -35,9 +35,11 @@ export function printMerklePersistHelp() {
35
35
  --dry-run Resolve bindings and emit the exact storage/index plan without writes
36
36
  --json Machine-readable output
37
37
 
38
- The SDK never infers owner identity. Hosts should pass authenticated owner_user_id.
39
- Physical bucket/database names come from Wrangler bindings, so customer installs keep
40
- WEBSITE_ASSETS/DB while selecting their own storage resources.
38
+ CLI ownership comes from the authenticated AgentSam session, while repository identity is
39
+ derived from Git/provider identity (with the committed project manifest as local fallback).
40
+ Programmatic hosts pass account_id + repository_id directly to the persistence plan. Physical
41
+ bucket/database names come from Wrangler bindings, so customer installs keep WEBSITE_ASSETS/DB
42
+ while selecting their own storage resources.
41
43
  `);
42
44
  }
43
45
 
@@ -55,7 +57,7 @@ function parse(args) {
55
57
  if (arg === '--dry-run') { opts.dryRun = true; continue; }
56
58
  if (arg === '--r2-only') { opts.r2Only = true; continue; }
57
59
  const map = {
58
- '--wrangler-config': 'wranglerConfig', '--owner-user-id': 'ownerUserId', '--repo-id': 'repoId',
60
+ '--wrangler-config': 'wranglerConfig',
59
61
  '--capture-kind': 'captureKind', '--connection-id': 'connectionId', '--runtime-lease-id': 'runtimeLeaseId',
60
62
  '--deployment-id': 'deploymentId', '--worker-version': 'workerVersionId', '--reference-label': 'referenceLabel',
61
63
  '--source': 'source', '--prefix': 'storagePrefix', '--r2-binding': 'r2Binding', '--d1-binding': 'd1Binding',
@@ -65,19 +67,36 @@ function parse(args) {
65
67
  throw new Error(`Unknown merkle persist option: ${arg}`);
66
68
  }
67
69
  if (!opts.snapshotPath) throw new Error('snapshot_file_required');
68
- opts.ownerUserId ||= process.env.AGENTSAM_OWNER_USER_ID || '';
69
70
  opts.wranglerConfig ||= process.env.AGENTSAM_WRANGLER_CONFIG || '';
70
71
  opts.connectionId ||= process.env.AGENTSAM_CONNECTION_ID || '';
71
72
  opts.runtimeLeaseId ||= process.env.AGENTSAM_RUNTIME_LEASE_ID || '';
72
73
  return opts;
73
74
  }
74
75
 
76
+ export function resolveMerklePersistenceIdentity(root, options = {}) {
77
+ const session = options.session ?? readAccountSession(options.sessionOptions || {});
78
+ const accountId = String(session?.account_id || '').trim();
79
+ if (!accountId) throw new Error('agentsam_login_required_for_merkle_persistence');
80
+
81
+ const projectConfig = options.projectConfig ?? tryReadProjectConfig(root);
82
+ const gitRepositoryId = portableRepositoryIdFromGit(root);
83
+ const repositoryId = gitRepositoryId || getRepositoryId(projectConfig);
84
+ if (!repositoryId) throw new Error('repository_identity_unresolved');
85
+
86
+ return {
87
+ accountId,
88
+ repositoryId,
89
+ repositoryIdentitySource: gitRepositoryId ? 'git' : 'project_manifest',
90
+ };
91
+ }
92
+
75
93
  export async function runMerklePersist(args = []) {
76
94
  const opts = parse(args);
77
95
  if (opts.help) { printMerklePersistHelp(); return null; }
78
96
  const snapshotPath = path.resolve(opts.snapshotPath);
79
97
  const snapshot = await readSnapshot(snapshotPath);
80
98
  const root = path.resolve(opts.root || snapshot.rootPath || process.cwd());
99
+ const identity = resolveMerklePersistenceIdentity(root);
81
100
  const wrangler = resolveWranglerMerklePersistence({
82
101
  configPath: opts.wranglerConfig,
83
102
  environment: opts.environment || null,
@@ -88,8 +107,8 @@ export async function runMerklePersist(args = []) {
88
107
  const plan = buildMerklePersistencePlan({
89
108
  snapshot,
90
109
  root,
91
- ownerUserId: opts.ownerUserId,
92
- repoId: opts.repoId,
110
+ accountId: identity.accountId,
111
+ repositoryId: identity.repositoryId,
93
112
  source: opts.source,
94
113
  captureKind: opts.captureKind,
95
114
  connectionId: opts.connectionId,
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { buildMerkleTree, saveSnapshot, readSnapshot, diffTrees, normalizePolicy } from '../lib/merkle/index.js';
3
+ import { buildMerkleTree, saveSnapshot, readSnapshot, diffTrees, normalizePolicy } from '../../packages/agentsam-repository/src/merkle/index.js';
4
4
  import { renderSummary, safeText } from '../ui/merkle/render.js';
5
5
  import { runMerkleExplorer } from '../ui/merkle/explorer.js';
6
6
  import { runMerklePersist } from './merkle-persist.js';
@@ -1,33 +1,109 @@
1
1
  import pc from 'picocolors';
2
2
  import { probeOllama, resolveOllamaConfig } from './ollama.js';
3
+ import { listModelCatalog } from '../models/index.js';
4
+ import { discoverProviderModels } from '../models/discovery.js';
5
+ import { resolveProviderCredential } from '../lib/provider-credentials.js';
3
6
 
4
- const API_PROVIDERS = Object.freeze([
7
+ export const API_PROVIDERS = Object.freeze([
5
8
  { id: 'openai', label: 'OpenAI', credential: 'OPENAI_API_KEY' },
9
+ { id: 'anthropic', label: 'Anthropic', credential: 'ANTHROPIC_API_KEY' },
6
10
  { id: 'gemini', label: 'Gemini', credential: 'GEMINI_API_KEY' },
7
- { id: 'grok', label: 'Grok', credential: 'XAI_API_KEY' },
11
+ { id: 'grok', label: 'Grok / xAI', credential: 'XAI_API_KEY' },
12
+ { id: 'cloudflare', label: 'Cloudflare', credential: 'CLOUDFLARE_API_TOKEN' },
8
13
  ]);
9
14
 
10
- function clean(value) {
11
- return value == null ? '' : String(value).trim();
15
+ function clean(value) { return value == null ? '' : String(value).trim(); }
16
+
17
+ function emptyDiscovery() {
18
+ return { attempted: false, ok: false, models: [], error: null };
12
19
  }
13
20
 
14
- function configured(value) {
15
- return Boolean(clean(value));
21
+ function providerSummary(result = {}) {
22
+ return {
23
+ attempted: result.attempted === true,
24
+ ok: result.ok === true,
25
+ error: result.error || null,
26
+ returnedModelCount: Array.isArray(result.models) ? result.models.length : 0,
27
+ };
28
+ }
29
+
30
+ function mergeStaticFallbacks(discovered = []) {
31
+ const byKey = new Map(discovered.map((row) => [row.model_key, row]));
32
+ const out = [...discovered];
33
+ for (const record of listModelCatalog()) {
34
+ if (byKey.has(record.model_key)) continue;
35
+ out.push({
36
+ model_key: record.model_key,
37
+ provider: record.provider,
38
+ provider_model_id: record.provider_model_id,
39
+ label: record.label,
40
+ availability: 'unverified',
41
+ availability_source: 'sdk_reference',
42
+ context_window: record.context_window,
43
+ context_window_source: 'sdk_reference',
44
+ max_output_tokens: record.max_output_tokens,
45
+ max_output_tokens_source: 'sdk_reference',
46
+ reasoning_efforts: [...record.reasoning_efforts],
47
+ service_tiers: [...record.service_tiers],
48
+ capabilities: { ...record.capabilities },
49
+ pricing: record.pricing || null,
50
+ context_policy: record.context_policy || null,
51
+ batch: record.batch || null,
52
+ source: record.source || null,
53
+ });
54
+ }
55
+ return out;
16
56
  }
17
57
 
18
58
  export async function collectModelsStatus(options = {}) {
19
59
  const env = options.env || process.env;
20
- const fetchImpl = options.fetchImpl || fetch;
60
+ const ollamaFetchImpl = options.fetchImpl || fetch;
61
+ const providerFetchImpl = options.providerFetchImpl || fetch;
21
62
  const ollamaConfig = resolveOllamaConfig({}, env);
22
- const ollama = await probeOllama(ollamaConfig, fetchImpl);
63
+ const ollama = await probeOllama(ollamaConfig, ollamaFetchImpl);
64
+ const credentials = new Map(
65
+ API_PROVIDERS.map((provider) => [
66
+ provider.id,
67
+ resolveProviderCredential(provider.id, { env, home: options.home }),
68
+ ]),
69
+ );
23
70
 
24
- return {
25
- schemaVersion: 'agentsam-model-inventory-v1',
26
- providers: API_PROVIDERS.map((provider) => ({
71
+ const providers = API_PROVIDERS.map((provider) => {
72
+ const credential = credentials.get(provider.id);
73
+ return {
27
74
  ...provider,
28
- configured: configured(env[provider.credential]),
29
- source: 'environment',
30
- })),
75
+ configured: credential?.configured === true,
76
+ source: credential?.source || null,
77
+ credentialError: credential?.error || null,
78
+ };
79
+ });
80
+
81
+ const shouldDiscover = options.discoverRemote !== false;
82
+ const discovery = {};
83
+ const providerModels = {};
84
+
85
+ await Promise.all(API_PROVIDERS.map(async (provider) => {
86
+ const credential = credentials.get(provider.id);
87
+ const result = shouldDiscover && credential?.configured
88
+ ? await discoverProviderModels(provider.id, credential, { fetchImpl: providerFetchImpl })
89
+ : emptyDiscovery();
90
+ discovery[provider.id] = providerSummary(result);
91
+ providerModels[provider.id] = result.models || [];
92
+ }));
93
+
94
+ const discovered = Object.values(providerModels).flat();
95
+ const exactAvailable = discovered.filter((row) => row.availability === 'available');
96
+ const catalogModels = mergeStaticFallbacks(discovered);
97
+
98
+ return {
99
+ schemaVersion: 'agentsam-model-inventory-v3',
100
+ generatedAt: new Date().toISOString(),
101
+ authority: 'per_credential_provider_discovery',
102
+ providers,
103
+ discovery,
104
+ providerModels,
105
+ catalogModels,
106
+ availableModels: exactAvailable,
31
107
  local: {
32
108
  provider: 'ollama',
33
109
  configured: ollama.online,
@@ -41,67 +117,94 @@ export async function collectModelsStatus(options = {}) {
41
117
  };
42
118
  }
43
119
 
44
- function statusMark(ok) {
45
- return ok ? pc.green('●') : pc.dim('○');
46
- }
47
-
48
- function writeLine(write, value = '') {
49
- write(`${value}\n`);
120
+ function statusMark(ok) { return ok ? pc.green('●') : pc.dim('○'); }
121
+ function writeLine(write, value = '') { write(`${value}\n`); }
122
+ function contextLabel(model) {
123
+ const value = Number(model?.context_window);
124
+ if (!Number.isFinite(value) || value <= 0) return 'ctx unknown';
125
+ return `ctx ${Math.round(value / 1000).toLocaleString('en-US')}k`;
50
126
  }
51
127
 
52
128
  export function renderModelsStatus(status) {
53
129
  const lines = [];
54
130
  lines.push('');
55
131
  lines.push(` ${pc.bold('Agent Sam · models')}`);
56
- lines.push(` ${pc.dim('Detected from this terminal session. Secrets are never printed.')}`);
132
+ lines.push(` ${pc.dim('Availability is verified with this machine\'s own provider credentials. Limits are provider-derived when exposed; otherwise marked unknown/reference.')}`);
57
133
  lines.push('');
58
134
 
59
135
  for (const provider of status.providers) {
60
- const state = provider.configured ? pc.green('configured') : pc.dim('not configured');
61
- const detail = provider.configured ? 'credential available' : provider.credential;
62
- lines.push(` ${statusMark(provider.configured)} ${pc.cyan(provider.label.padEnd(10))} ${state.padEnd(20)} ${pc.dim(detail)}`);
136
+ const state = provider.configured
137
+ ? pc.green('configured')
138
+ : pc.dim(provider.credentialError ? 'blocked' : 'not configured');
139
+ const discovery = status.discovery?.[provider.id];
140
+ const detail = provider.configured
141
+ ? discovery?.attempted
142
+ ? discovery.ok
143
+ ? `${discovery.returnedModelCount} account-visible models`
144
+ : `discovery failed · ${discovery.error || 'unknown error'}`
145
+ : `credential available · ${provider.source || 'runtime'}`
146
+ : provider.credentialError
147
+ ? `${provider.credential} · ${provider.credentialError}`
148
+ : provider.credential;
149
+ lines.push(` ${statusMark(provider.configured)} ${pc.cyan(provider.label.padEnd(12))} ${state.padEnd(20)} ${pc.dim(detail)}`);
150
+ }
151
+
152
+ for (const provider of API_PROVIDERS) {
153
+ const models = status.providerModels?.[provider.id] || [];
154
+ if (!models.length) continue;
155
+ lines.push('');
156
+ lines.push(` ${pc.dim(`${provider.label} · provider-verified for this credential`)}`);
157
+ const visible = models.slice(0, 30);
158
+ for (const model of visible) {
159
+ const source = model.context_window_source === 'provider_api'
160
+ ? contextLabel(model)
161
+ : model.context_window_source === 'sdk_reference'
162
+ ? `${contextLabel(model)} · reference`
163
+ : 'ctx unknown';
164
+ lines.push(` ${pc.green('•')} ${model.provider_model_id} ${pc.dim('· ' + source)}`);
165
+ }
166
+ if (models.length > visible.length) lines.push(` ${pc.dim(`… ${models.length - visible.length} more`)}`);
63
167
  }
64
168
 
65
169
  const local = status.local;
66
170
  const localState = local.online ? pc.green('online') : pc.dim('offline');
67
- lines.push(` ${statusMark(local.online)} ${pc.cyan('Ollama'.padEnd(10))} ${localState.padEnd(20)} ${pc.dim('local only')}`);
68
-
171
+ lines.push('');
172
+ lines.push(` ${statusMark(local.online)} ${pc.cyan('Ollama'.padEnd(12))} ${localState.padEnd(20)} ${pc.dim('local only')}`);
69
173
  if (local.online) {
70
174
  const names = local.models.map((row) => row.name).filter(Boolean);
71
- lines.push('');
72
- lines.push(` ${pc.dim('local models')}`);
73
- if (names.length) {
74
- for (const name of names) lines.push(` ${pc.green('•')} ${name}`);
75
- } else {
76
- lines.push(` ${pc.dim('no models reported')}`);
77
- }
78
- lines.push('');
79
- lines.push(` ${pc.dim('chat default')} ${local.chatModel}`);
80
- lines.push(` ${pc.dim('embed default')} ${local.embedModel}`);
175
+ for (const name of names.slice(0, 30)) lines.push(` ${pc.green('•')} ${name}`);
176
+ if (names.length > 30) lines.push(` ${pc.dim(`… ${names.length - 30} more`)}`);
81
177
  }
82
178
 
83
179
  lines.push('');
84
- lines.push(` ${pc.dim('Provider model catalogs and model selection belong to the connected host/runtime;')}`);
85
- lines.push(` ${pc.dim('this command only reports what this local CLI can prove is configured or available.')}`);
180
+ lines.push(` ${pc.dim('Use /model inside Agent Sam. The picker is built from the models visible to your own connected provider credentials.')}`);
86
181
  lines.push('');
87
182
  return lines.join('\n');
88
183
  }
89
184
 
90
185
  export async function runModels(argv = [], options = {}) {
91
186
  if (argv.some((arg) => arg === '--help' || arg === '-h')) {
92
- const text = 'agentsam models [--json]\n\nShow model providers configured in this terminal session plus models reported by local Ollama.\n';
187
+ const text = [
188
+ 'agentsam models [--json] [--no-discover]',
189
+ '',
190
+ 'Probe the provider accounts configured on this machine and list the models visible to those exact credentials.',
191
+ 'Context/output limits are taken from provider metadata when available; otherwise Agent Sam reports unknown or a clearly-labeled SDK reference.',
192
+ '',
193
+ ].join('\n');
93
194
  (options.write || process.stdout.write.bind(process.stdout))(text);
94
195
  return;
95
196
  }
96
- const unknown = argv.filter((arg) => arg !== '--json');
197
+
198
+ const allowed = new Set(['--json', '--no-discover']);
199
+ const unknown = argv.filter((arg) => !allowed.has(arg));
97
200
  if (unknown.length) throw new Error(`unknown models option: ${unknown[0]}`);
98
201
 
99
- const status = await collectModelsStatus(options);
202
+ const status = await collectModelsStatus({
203
+ ...options,
204
+ discoverRemote: !argv.includes('--no-discover'),
205
+ });
100
206
  const write = options.write || ((text) => process.stdout.write(text));
101
- if (argv.includes('--json')) {
102
- writeLine(write, JSON.stringify(status, null, 2));
103
- } else {
104
- write(renderModelsStatus(status));
105
- }
207
+ if (argv.includes('--json')) writeLine(write, JSON.stringify(status, null, 2));
208
+ else write(renderModelsStatus(status));
106
209
  return status;
107
210
  }
@@ -148,6 +148,32 @@ function hasModel(models, desired) {
148
148
  return models.some((row) => normalizeModelName(row.name || row.model) === want);
149
149
  }
150
150
 
151
+ export async function probeOllamaModel(model, config, fetchImpl = fetch) {
152
+ const endpoint = `${config.baseUrl}/api/show`;
153
+ try {
154
+ const response = await fetchImpl(endpoint, {
155
+ method: 'POST',
156
+ headers: { 'content-type': 'application/json' },
157
+ body: JSON.stringify({ model }),
158
+ signal: AbortSignal.timeout(3000),
159
+ });
160
+ if (!response.ok) return { ok: false, model, context_window: null, error: `HTTP ${response.status}` };
161
+ const body = await response.json();
162
+ const info = body?.model_info && typeof body.model_info === 'object' ? body.model_info : {};
163
+ const contextEntry = Object.entries(info).find(([key, value]) => key.endsWith('.context_length') && Number(value) > 0);
164
+ return {
165
+ ok: true,
166
+ model,
167
+ context_window: contextEntry ? Math.floor(Number(contextEntry[1])) : null,
168
+ context_window_source: contextEntry ? 'local_runtime' : 'unknown',
169
+ capabilities: Array.isArray(body?.capabilities) ? body.capabilities : [],
170
+ details: body?.details || null,
171
+ };
172
+ } catch (error) {
173
+ return { ok: false, model, context_window: null, error: error?.message || String(error) };
174
+ }
175
+ }
176
+
151
177
  export async function probeOllama(config, fetchImpl = fetch) {
152
178
  const endpoint = `${config.baseUrl}/api/tags`;
153
179
  try {
@@ -1,9 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { cancel, intro, isCancel, outro, select, text } from '@clack/prompts';
4
+ import { cancel, confirm, intro, isCancel, outro, select, text } from '@clack/prompts';
5
5
  import { collectModelsStatus } from './models.js';
6
+ import { getModelRecord } from '../models/index.js';
6
7
  import { detectCliProject, readCliPreferences, writeCliPreferences } from '../lib/cli-preferences.js';
8
+ import { readAccountSession } from '../lib/account-session.js';
7
9
 
8
10
  function stopIfCancelled(value) {
9
11
  if (!isCancel(value)) return value;
@@ -18,85 +20,152 @@ function commandExists(command) {
18
20
  return spawnSync(probe[0], probe[1], { stdio: 'ignore' }).status === 0;
19
21
  }
20
22
 
23
+ export function runtimeOptions({ accountConnected = false } = {}) {
24
+ const rows = [{ value: 'local', label: 'Local machine', hint: 'standalone · real shell + filesystem' }];
25
+ if (accountConnected) {
26
+ rows.push(
27
+ { value: 'remote', label: 'Remote', hint: 'IAM-connected enrolled runtime' },
28
+ { value: 'sandbox', label: 'Sandbox', hint: 'IAM-connected isolated runtime' },
29
+ );
30
+ }
31
+ return rows;
32
+ }
33
+
21
34
  export function availableShells(env = process.env) {
22
35
  const values = [];
23
36
  const detected = path.basename(env.SHELL || env.ComSpec || '').trim();
24
37
  if (detected) values.push(detected);
25
- for (const shell of ['zsh', 'bash', 'fish', 'pwsh']) {
26
- if (!values.includes(shell) && commandExists(shell)) values.push(shell);
27
- }
38
+ for (const shell of ['zsh', 'bash', 'fish', 'pwsh']) if (!values.includes(shell) && commandExists(shell)) values.push(shell);
28
39
  return values.length ? values : ['shell'];
29
40
  }
30
41
 
31
- function modelOptions(status) {
32
- const options = [{ value: 'auto', label: 'Automatic', hint: 'connected host/runtime decides' }];
33
- if (status.local?.online) {
34
- for (const row of status.local.models || []) {
35
- if (row?.name) options.push({ value: `ollama:${row.name}`, label: `Ollama · ${row.name}`, hint: 'local' });
36
- }
42
+ export function modelOptions(status) {
43
+ const options = [{ value: 'auto', label: 'Automatic', hint: 'runtime chooses from this credential\'s verified inventory', model: null }];
44
+ for (const model of status.availableModels || []) {
45
+ const context = Number(model.context_window) > 0 ? ` · ctx ${Math.round(Number(model.context_window) / 1000)}k` : ' · ctx unknown';
46
+ options.push({
47
+ value: model.model_key,
48
+ label: `${model.provider} · ${model.label}`,
49
+ hint: `${model.provider_model_id}${context} · provider verified`,
50
+ model,
51
+ });
37
52
  }
38
- for (const provider of status.providers || []) {
39
- if (provider.configured) options.push({ value: `${provider.id}:auto`, label: provider.label, hint: 'provider configured' });
53
+ if (status.local?.online) {
54
+ for (const row of status.local.models || []) if (row?.name) options.push({
55
+ value: `ollama:${row.name}`,
56
+ label: `Ollama · ${row.name}`,
57
+ hint: 'local',
58
+ model: {
59
+ model_key: `ollama:${row.name}`, provider: 'ollama', provider_model_id: row.name, label: row.name,
60
+ availability: 'available', availability_source: 'local_runtime', context_window: null, context_window_source: 'unknown',
61
+ max_output_tokens: null, max_output_tokens_source: 'unknown', reasoning_efforts: ['auto'], service_tiers: ['default'], capabilities: { chat: true },
62
+ },
63
+ });
40
64
  }
41
65
  return options;
42
66
  }
43
67
 
44
- export async function configureCliPreferences({ cwd = process.cwd(), firstRun = false } = {}) {
45
- let identity = detectCliProject(cwd);
46
- const existing = readCliPreferences(identity.root) || {};
47
- intro(firstRun ? 'Welcome to Agent Sam' : 'Agent Sam settings');
48
-
49
- const projectChoice = stopIfCancelled(await select({
50
- message: 'Where should Agent Sam work?',
51
- initialValue: 'current',
52
- options: [
53
- { value: 'current', label: `This project · ${identity.project}`, hint: identity.root },
54
- { value: 'other', label: 'Choose another folder' },
55
- ],
56
- }));
57
-
58
- if (projectChoice === 'other') {
59
- const folder = stopIfCancelled(await text({
60
- message: 'Project folder',
61
- placeholder: identity.root,
62
- defaultValue: identity.root,
63
- validate(value) {
64
- const resolved = path.resolve(String(value || ''));
65
- if (!fs.existsSync(resolved)) return 'Folder does not exist';
66
- if (!fs.statSync(resolved).isDirectory()) return 'Path is not a directory';
67
- },
68
- }));
69
- identity = detectCliProject(path.resolve(String(folder)));
70
- }
68
+ function resolvedModel(modelPreference, modelSnapshot) {
69
+ if (modelSnapshot?.model_key === modelPreference) return modelSnapshot;
70
+ return getModelRecord(modelPreference);
71
+ }
71
72
 
72
- const runtime = stopIfCancelled(await select({
73
- message: 'Runtime',
74
- initialValue: existing.runtime || 'local',
75
- options: [
76
- { value: 'local', label: 'Local machine', hint: 'real shell + filesystem' },
77
- { value: 'remote', label: 'Remote', hint: 'connected remote runtime' },
78
- { value: 'sandbox', label: 'Sandbox', hint: 'isolated disposable runtime' },
79
- ],
73
+ function reasoningOptions(modelPreference, modelSnapshot) {
74
+ const record = resolvedModel(modelPreference, modelSnapshot);
75
+ if (!record) return [{ value: 'auto', label: 'Automatic', hint: 'runtime/provider default' }];
76
+ return record.reasoning_efforts.map((value) => ({
77
+ value,
78
+ label: value === 'xhigh' ? 'Extra high' : value === 'max' ? 'Max' : value[0].toUpperCase() + value.slice(1),
79
+ hint: value === 'low' ? 'lighter reasoning' : value === 'max' ? 'highest supported reasoning depth' : undefined,
80
80
  }));
81
+ }
81
82
 
82
- const shells = availableShells();
83
- const terminal = stopIfCancelled(await select({
84
- message: 'Terminal',
85
- initialValue: existing.terminal && shells.includes(existing.terminal) ? existing.terminal : shells[0],
86
- options: shells.map((shell) => ({ value: shell, label: shell, hint: shell === shells[0] ? 'detected/default' : undefined })),
87
- }));
83
+ function serviceTierOptions(modelPreference, modelSnapshot) {
84
+ const record = resolvedModel(modelPreference, modelSnapshot);
85
+ if (!record) return [{ value: 'default', label: 'Standard', hint: 'default provider processing' }];
86
+ return record.service_tiers.map((value) => {
87
+ if (value === 'fast') return { value, label: 'Fast', hint: 'lower latency · 2× applicable token rates for Astra' };
88
+ if (value === 'flex') return { value, label: 'Flex', hint: 'slower / capacity-sensitive · 50% of Standard for Astra' };
89
+ return { value, label: 'Standard', hint: 'standard pricing and latency' };
90
+ });
91
+ }
88
92
 
89
- let status = { providers: [], local: { online: false, models: [] } };
90
- try { status = await collectModelsStatus(); } catch { /* inventory is best-effort */ }
93
+ async function promptModelPreferences(identity, existing, options = {}) {
94
+ let status = { providers: [], availableModels: [], local: { online: false, models: [] } };
95
+ try { status = await collectModelsStatus(options.modelStatusOptions || {}); } catch { /* inventory remains best-effort */ }
91
96
  const models = modelOptions(status);
92
97
  const initialModel = models.some((row) => row.value === existing.modelPreference) ? existing.modelPreference : 'auto';
93
- const modelPreference = stopIfCancelled(await select({
94
- message: 'Model preference',
95
- initialValue: initialModel,
96
- options: models,
97
- }));
98
+ const modelPreference = stopIfCancelled(await select({ message: 'Model', initialValue: initialModel, options: models.map(({ model, ...row }) => row) }));
99
+ const selected = models.find((row) => row.value === modelPreference)?.model || null;
100
+ const modelSnapshot = selected || (existing.modelSnapshot?.model_key === modelPreference ? existing.modelSnapshot : null);
101
+
102
+ const reasoning = reasoningOptions(modelPreference, modelSnapshot);
103
+ const initialReasoning = reasoning.some((row) => row.value === existing.reasoningEffort) ? existing.reasoningEffort : reasoning[0].value;
104
+ const reasoningEffort = stopIfCancelled(await select({ message: 'Reasoning level', initialValue: initialReasoning, options: reasoning }));
105
+
106
+ const tiers = serviceTierOptions(modelPreference, modelSnapshot);
107
+ const initialTier = tiers.some((row) => row.value === existing.serviceTier) ? existing.serviceTier : tiers[0].value;
108
+ const serviceTier = stopIfCancelled(await select({ message: 'Processing', initialValue: initialTier, options: tiers }));
109
+
110
+ return { modelPreference, modelSnapshot, reasoningEffort, serviceTier };
111
+ }
112
+
113
+ export async function configureCliPreferences({ cwd = process.cwd(), firstRun = false, section = 'all', modelStatusOptions, home, env = process.env } = {}) {
114
+ let identity = detectCliProject(cwd);
115
+ const existing = readCliPreferences(identity.root) || {};
116
+ intro(firstRun ? `You are in ${identity.root}` : section === 'model' ? 'Agent Sam model' : 'Agent Sam settings');
117
+
118
+ let trustedDirectory = existing.trustedDirectory === true;
119
+ if (firstRun && !trustedDirectory) {
120
+ trustedDirectory = stopIfCancelled(await confirm({
121
+ message: 'Do you trust the contents of this directory? Project-local instructions, hooks, and execution policy may load.',
122
+ initialValue: false,
123
+ }));
124
+ if (!trustedDirectory) {
125
+ cancel('Directory not trusted. Agent Sam did not load project-local configuration.');
126
+ const error = new Error('directory_not_trusted');
127
+ error.code = 'AGENTSAM_SETUP_CANCELLED';
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ const accountConnected = Boolean(readAccountSession({ home, env })?.account_id);
133
+ const runtimes = runtimeOptions({ accountConnected });
134
+ let runtime = runtimes.some((row) => row.value === existing.runtime) ? existing.runtime : 'local';
135
+ let terminal = existing.terminal || availableShells(env)[0];
136
+ if (!firstRun && section === 'all') {
137
+ const projectChoice = stopIfCancelled(await select({
138
+ message: 'Where should Agent Sam work?',
139
+ initialValue: 'current',
140
+ options: [
141
+ { value: 'current', label: `This project · ${identity.project}`, hint: identity.root },
142
+ { value: 'other', label: 'Choose another folder' },
143
+ ],
144
+ }));
145
+ if (projectChoice === 'other') {
146
+ const folder = stopIfCancelled(await text({
147
+ message: 'Project folder', placeholder: identity.root, defaultValue: identity.root,
148
+ validate(value) {
149
+ const resolved = path.resolve(String(value || ''));
150
+ if (!fs.existsSync(resolved)) return 'Folder does not exist';
151
+ if (!fs.statSync(resolved).isDirectory()) return 'Path is not a directory';
152
+ },
153
+ }));
154
+ identity = detectCliProject(path.resolve(String(folder)));
155
+ }
156
+ runtime = stopIfCancelled(await select({
157
+ message: 'Runtime', initialValue: runtime,
158
+ options: runtimes,
159
+ }));
160
+ const shells = availableShells(env);
161
+ terminal = stopIfCancelled(await select({
162
+ message: 'Terminal', initialValue: terminal && shells.includes(terminal) ? terminal : shells[0],
163
+ options: shells.map((shell) => ({ value: shell, label: shell, hint: shell === shells[0] ? 'detected/default' : undefined })),
164
+ }));
165
+ }
98
166
 
99
- const preferences = writeCliPreferences(identity.root, { runtime, terminal, modelPreference });
167
+ const model = await promptModelPreferences(identity, existing, { modelStatusOptions: { home, env, ...(modelStatusOptions || {}) } });
168
+ const preferences = writeCliPreferences(identity.root, { trustedDirectory, runtime, terminal, ...model });
100
169
  outro(`Ready · ${identity.project}`);
101
170
  return { identity, preferences };
102
171
  }