@inneranimalmedia/agentsam-sdk 2.6.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.
- package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
- package/docs/RELEASES.md +7 -7
- package/docs/SOURCE_ARCHITECTURE.md +58 -0
- package/docs/TEST_TIERS.md +26 -0
- package/migrations/runtime/0001_cli_runtime.sql +298 -0
- package/package.json +28 -7
- package/packages/agentsam-repository/README.md +15 -0
- package/packages/agentsam-repository/package.json +25 -0
- package/packages/agentsam-repository/src/contracts.js +113 -0
- package/packages/agentsam-repository/src/index.js +3 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
- package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
- package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
- package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
- package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
- package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
- package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
- package/packages/identity/package.json +1 -1
- package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
- package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
- package/protocol/repository/repository-contract.schema.json +24 -0
- package/protocol/repository/repository-dependency.schema.json +24 -0
- package/protocol/repository/repository-identity.schema.json +17 -0
- package/protocol/rpc/v1/common.proto +16 -0
- package/protocol/rpc/v1/errors.proto +35 -0
- package/protocol/rpc/v1/knowledge.proto +77 -0
- package/services/knowledge/package-lock.json +333 -0
- package/services/knowledge/package.json +5 -1
- package/src/agent/responses-runner.js +63 -35
- package/src/capabilities/repository-snapshot.js +3 -3
- package/src/cli.js +23 -6
- package/src/commands/context-economics.js +17 -2
- package/src/commands/context.js +1 -1
- package/src/commands/db.js +20 -3
- package/src/commands/env.js +90 -0
- package/src/commands/knowledge.js +12 -4
- package/src/commands/merkle-persist.js +30 -11
- package/src/commands/merkle.js +1 -1
- package/src/commands/models.js +123 -65
- package/src/commands/ollama.js +26 -0
- package/src/commands/preferences.js +53 -26
- package/src/commands/shell.js +236 -48
- package/src/errors/contract.js +236 -0
- package/src/errors/index.js +14 -0
- package/src/index.js +13 -1
- package/src/knowledge/service/auth.js +13 -0
- package/src/knowledge/service/grpc-client.js +115 -0
- package/src/knowledge/service/grpc-codec.js +237 -0
- package/src/knowledge/service/grpc-server.js +83 -0
- package/src/knowledge/service/job-engine.js +248 -0
- package/src/knowledge/service/server.js +87 -135
- package/src/knowledge/source.js +1 -1
- package/src/lib/cli-preferences.js +31 -4
- package/src/lib/deploy-receipt/index.js +2 -2
- package/src/lib/knowledge-docker.js +6 -3
- package/src/lib/local-sessions.js +23 -2
- package/src/lib/local-status.js +1 -1
- package/src/lib/project-config.js +1 -1
- package/src/lib/provider-credentials.js +105 -5
- package/src/lib/slash-commands.js +4 -3
- package/src/local/migrations.js +93 -0
- package/src/local/runtime-store.js +141 -0
- package/src/local/sqlite.js +2 -0
- package/src/local-pty/server.js +113 -51
- package/src/models/discovery.js +292 -0
- package/src/providers/anthropic-messages.js +192 -0
- package/src/providers/cloudflare-chat.js +183 -0
- package/src/providers/factory.js +69 -0
- package/src/providers/gemini-generate-content.js +208 -0
- package/src/providers/index.js +5 -0
- package/src/providers/ollama-chat.js +148 -0
- package/src/providers/openai-responses.js +226 -75
- package/src/repository/index.js +14 -2
- package/src/rpc/generated/common_grpc_pb.js +1 -0
- package/src/rpc/generated/common_pb.js +536 -0
- package/src/rpc/generated/errors_grpc_pb.js +1 -0
- package/src/rpc/generated/errors_pb.js +482 -0
- package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
- package/src/rpc/generated/knowledge_pb.js +2168 -0
- package/src/rpc/generated/package.json +3 -0
- package/src/security/trust-boundary.js +2 -2
- package/src/telemetry/events.js +4 -1
- package/src/ui/cli/activity.js +76 -0
- package/src/ui/cli/compaction.js +15 -0
- package/src/ui/cli/footer.js +39 -0
- package/src/ui/cli/help.js +192 -0
- package/src/ui/cli/plan.js +20 -0
- package/src/ui/cli/runtime-events.js +110 -0
- package/src/ui/cli/waiting.js +16 -0
- package/src/ui/merkle/render.js +1 -1
- package/test/cli/preferences-runtime.test.mjs +11 -0
- package/test/cli/runtime-ui.test.mjs +74 -0
- package/test/error-diagnostics.test.mjs +57 -1
- package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
- package/test/integration/cli-help.test.mjs +37 -0
- package/test/integration/knowledge-rpc.test.mjs +112 -0
- package/test/integration/merkle-cli.test.mjs +61 -0
- package/test/integration/merkle-persistence-identity.test.mjs +48 -0
- package/test/integration/provider-env-cli.test.mjs +49 -0
- package/test/integration/provider-factory.test.mjs +197 -0
- package/test/integration/repository-company-graph.test.mjs +90 -0
- package/test/integration/runtime-migrations.test.mjs +82 -0
- package/test/knowledge-service.test.mjs +5 -0
- package/test/knowledge.test.mjs +16 -0
- package/test/live/terminal-transport.live.test.mjs +24 -0
- package/test/local-sessions.test.mjs +7 -1
- package/test/models.test.mjs +101 -4
- package/test/ollama.test.mjs +21 -0
- package/test/portable-context.test.mjs +1 -1
- package/test/provider-credentials.test.mjs +45 -1
- package/test/release-hygiene.test.mjs +13 -5
- package/test/responses-runner.test.mjs +3 -1
- package/test/shell.test.mjs +50 -8
- package/test/terminal/local-pty.mock.test.mjs +151 -0
- /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
package/src/commands/merkle.js
CHANGED
|
@@ -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 '
|
|
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';
|
package/src/commands/models.js
CHANGED
|
@@ -1,32 +1,58 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
import { probeOllama, resolveOllamaConfig } from './ollama.js';
|
|
3
3
|
import { listModelCatalog } from '../models/index.js';
|
|
4
|
+
import { discoverProviderModels } from '../models/discovery.js';
|
|
4
5
|
import { resolveProviderCredential } from '../lib/provider-credentials.js';
|
|
5
6
|
|
|
6
|
-
const API_PROVIDERS = Object.freeze([
|
|
7
|
+
export const API_PROVIDERS = Object.freeze([
|
|
7
8
|
{ id: 'openai', label: 'OpenAI', credential: 'OPENAI_API_KEY' },
|
|
8
|
-
{ id: 'gemini', label: 'Gemini', credential: 'GEMINI_API_KEY' },
|
|
9
|
-
{ id: 'grok', label: 'Grok', credential: 'XAI_API_KEY' },
|
|
10
9
|
{ id: 'anthropic', label: 'Anthropic', credential: 'ANTHROPIC_API_KEY' },
|
|
10
|
+
{ id: 'gemini', label: 'Gemini', credential: 'GEMINI_API_KEY' },
|
|
11
|
+
{ id: 'grok', label: 'Grok / xAI', credential: 'XAI_API_KEY' },
|
|
12
|
+
{ id: 'cloudflare', label: 'Cloudflare', credential: 'CLOUDFLARE_API_TOKEN' },
|
|
11
13
|
]);
|
|
12
14
|
|
|
13
15
|
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
|
|
17
|
+
function emptyDiscovery() {
|
|
18
|
+
return { attempted: false, ok: false, models: [], error: null };
|
|
19
|
+
}
|
|
20
|
+
|
|
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,
|
|
22
53
|
});
|
|
23
|
-
if (!response.ok) return { attempted: true, ok: false, models: [], error: `HTTP ${response.status}` };
|
|
24
|
-
const body = await response.json();
|
|
25
|
-
const models = Array.isArray(body?.data) ? body.data.map((row) => clean(row?.id)).filter(Boolean) : [];
|
|
26
|
-
return { attempted: true, ok: true, models, error: null };
|
|
27
|
-
} catch (error) {
|
|
28
|
-
return { attempted: true, ok: false, models: [], error: error?.message || String(error) };
|
|
29
54
|
}
|
|
55
|
+
return out;
|
|
30
56
|
}
|
|
31
57
|
|
|
32
58
|
export async function collectModelsStatus(options = {}) {
|
|
@@ -35,7 +61,13 @@ export async function collectModelsStatus(options = {}) {
|
|
|
35
61
|
const providerFetchImpl = options.providerFetchImpl || fetch;
|
|
36
62
|
const ollamaConfig = resolveOllamaConfig({}, env);
|
|
37
63
|
const ollama = await probeOllama(ollamaConfig, ollamaFetchImpl);
|
|
38
|
-
const credentials = new Map(
|
|
64
|
+
const credentials = new Map(
|
|
65
|
+
API_PROVIDERS.map((provider) => [
|
|
66
|
+
provider.id,
|
|
67
|
+
resolveProviderCredential(provider.id, { env, home: options.home }),
|
|
68
|
+
]),
|
|
69
|
+
);
|
|
70
|
+
|
|
39
71
|
const providers = API_PROVIDERS.map((provider) => {
|
|
40
72
|
const credential = credentials.get(provider.id);
|
|
41
73
|
return {
|
|
@@ -46,36 +78,32 @@ export async function collectModelsStatus(options = {}) {
|
|
|
46
78
|
};
|
|
47
79
|
});
|
|
48
80
|
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
availability: record.provider === 'openai' && openai.ok
|
|
61
|
-
? (availableIds.has(record.provider_model_id) ? 'available' : 'unavailable')
|
|
62
|
-
: 'unverified',
|
|
63
|
-
source: record.source,
|
|
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 || [];
|
|
64
92
|
}));
|
|
65
93
|
|
|
94
|
+
const discovered = Object.values(providerModels).flat();
|
|
95
|
+
const exactAvailable = discovered.filter((row) => row.availability === 'available');
|
|
96
|
+
const catalogModels = mergeStaticFallbacks(discovered);
|
|
97
|
+
|
|
66
98
|
return {
|
|
67
|
-
schemaVersion: 'agentsam-model-inventory-
|
|
99
|
+
schemaVersion: 'agentsam-model-inventory-v3',
|
|
100
|
+
generatedAt: new Date().toISOString(),
|
|
101
|
+
authority: 'per_credential_provider_discovery',
|
|
68
102
|
providers,
|
|
69
|
-
discovery
|
|
70
|
-
|
|
71
|
-
attempted: openai.attempted,
|
|
72
|
-
ok: openai.ok,
|
|
73
|
-
error: openai.error,
|
|
74
|
-
returnedModelCount: openai.models.length,
|
|
75
|
-
},
|
|
76
|
-
},
|
|
103
|
+
discovery,
|
|
104
|
+
providerModels,
|
|
77
105
|
catalogModels,
|
|
78
|
-
availableModels:
|
|
106
|
+
availableModels: exactAvailable,
|
|
79
107
|
local: {
|
|
80
108
|
provider: 'ollama',
|
|
81
109
|
configured: ollama.online,
|
|
@@ -91,60 +119,90 @@ export async function collectModelsStatus(options = {}) {
|
|
|
91
119
|
|
|
92
120
|
function statusMark(ok) { return ok ? pc.green('●') : pc.dim('○'); }
|
|
93
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`;
|
|
126
|
+
}
|
|
94
127
|
|
|
95
128
|
export function renderModelsStatus(status) {
|
|
96
129
|
const lines = [];
|
|
97
130
|
lines.push('');
|
|
98
131
|
lines.push(` ${pc.bold('Agent Sam · models')}`);
|
|
99
|
-
lines.push(` ${pc.dim('
|
|
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.')}`);
|
|
100
133
|
lines.push('');
|
|
101
134
|
|
|
102
135
|
for (const provider of status.providers) {
|
|
103
|
-
const state = provider.configured
|
|
104
|
-
|
|
105
|
-
|
|
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)}`);
|
|
106
150
|
}
|
|
107
151
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
lines.push(` ${pc.dim('provider-verified selectable models')}`);
|
|
112
|
-
for (const model of exact) lines.push(` ${pc.green('•')} ${model.provider_model_id}`);
|
|
113
|
-
} else if (status.discovery?.openai?.attempted) {
|
|
152
|
+
for (const provider of API_PROVIDERS) {
|
|
153
|
+
const models = status.providerModels?.[provider.id] || [];
|
|
154
|
+
if (!models.length) continue;
|
|
114
155
|
lines.push('');
|
|
115
|
-
lines.push(` ${pc.dim(
|
|
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`)}`);
|
|
116
167
|
}
|
|
117
168
|
|
|
118
169
|
const local = status.local;
|
|
119
170
|
const localState = local.online ? pc.green('online') : pc.dim('offline');
|
|
120
|
-
lines.push(
|
|
171
|
+
lines.push('');
|
|
172
|
+
lines.push(` ${statusMark(local.online)} ${pc.cyan('Ollama'.padEnd(12))} ${localState.padEnd(20)} ${pc.dim('local only')}`);
|
|
121
173
|
if (local.online) {
|
|
122
174
|
const names = local.models.map((row) => row.name).filter(Boolean);
|
|
123
|
-
lines.push('');
|
|
124
|
-
lines.push(`
|
|
125
|
-
if (names.length) for (const name of names) lines.push(` ${pc.green('•')} ${name}`);
|
|
126
|
-
else lines.push(` ${pc.dim('no models reported')}`);
|
|
127
|
-
lines.push('');
|
|
128
|
-
lines.push(` ${pc.dim('chat default')} ${local.chatModel}`);
|
|
129
|
-
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`)}`);
|
|
130
177
|
}
|
|
178
|
+
|
|
131
179
|
lines.push('');
|
|
132
|
-
lines.push(` ${pc.dim('Use /model inside Agent Sam
|
|
180
|
+
lines.push(` ${pc.dim('Use /model inside Agent Sam. The picker is built from the models visible to your own connected provider credentials.')}`);
|
|
133
181
|
lines.push('');
|
|
134
182
|
return lines.join('\n');
|
|
135
183
|
}
|
|
136
184
|
|
|
137
185
|
export async function runModels(argv = [], options = {}) {
|
|
138
186
|
if (argv.some((arg) => arg === '--help' || arg === '-h')) {
|
|
139
|
-
const text =
|
|
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');
|
|
140
194
|
(options.write || process.stdout.write.bind(process.stdout))(text);
|
|
141
195
|
return;
|
|
142
196
|
}
|
|
197
|
+
|
|
143
198
|
const allowed = new Set(['--json', '--no-discover']);
|
|
144
199
|
const unknown = argv.filter((arg) => !allowed.has(arg));
|
|
145
200
|
if (unknown.length) throw new Error(`unknown models option: ${unknown[0]}`);
|
|
146
201
|
|
|
147
|
-
const status = await collectModelsStatus({
|
|
202
|
+
const status = await collectModelsStatus({
|
|
203
|
+
...options,
|
|
204
|
+
discoverRemote: !argv.includes('--no-discover'),
|
|
205
|
+
});
|
|
148
206
|
const write = options.write || ((text) => process.stdout.write(text));
|
|
149
207
|
if (argv.includes('--json')) writeLine(write, JSON.stringify(status, null, 2));
|
|
150
208
|
else write(renderModelsStatus(status));
|
package/src/commands/ollama.js
CHANGED
|
@@ -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 {
|
|
@@ -5,6 +5,7 @@ import { cancel, confirm, intro, isCancel, outro, select, text } from '@clack/pr
|
|
|
5
5
|
import { collectModelsStatus } from './models.js';
|
|
6
6
|
import { getModelRecord } from '../models/index.js';
|
|
7
7
|
import { detectCliProject, readCliPreferences, writeCliPreferences } from '../lib/cli-preferences.js';
|
|
8
|
+
import { readAccountSession } from '../lib/account-session.js';
|
|
8
9
|
|
|
9
10
|
function stopIfCancelled(value) {
|
|
10
11
|
if (!isCancel(value)) return value;
|
|
@@ -19,6 +20,17 @@ function commandExists(command) {
|
|
|
19
20
|
return spawnSync(probe[0], probe[1], { stdio: 'ignore' }).status === 0;
|
|
20
21
|
}
|
|
21
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
|
+
|
|
22
34
|
export function availableShells(env = process.env) {
|
|
23
35
|
const values = [];
|
|
24
36
|
const detected = path.basename(env.SHELL || env.ComSpec || '').trim();
|
|
@@ -28,23 +40,38 @@ export function availableShells(env = process.env) {
|
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
export function modelOptions(status) {
|
|
31
|
-
const options = [{ value: 'auto', label: 'Automatic', hint: 'runtime chooses
|
|
43
|
+
const options = [{ value: 'auto', label: 'Automatic', hint: 'runtime chooses from this credential\'s verified inventory', model: null }];
|
|
32
44
|
for (const model of status.availableModels || []) {
|
|
33
|
-
|
|
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
|
+
});
|
|
34
52
|
}
|
|
35
53
|
if (status.local?.online) {
|
|
36
|
-
for (const row of status.local.models || []) if (row?.name) options.push({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
});
|
|
42
64
|
}
|
|
43
65
|
return options;
|
|
44
66
|
}
|
|
45
67
|
|
|
46
|
-
function
|
|
47
|
-
|
|
68
|
+
function resolvedModel(modelPreference, modelSnapshot) {
|
|
69
|
+
if (modelSnapshot?.model_key === modelPreference) return modelSnapshot;
|
|
70
|
+
return getModelRecord(modelPreference);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function reasoningOptions(modelPreference, modelSnapshot) {
|
|
74
|
+
const record = resolvedModel(modelPreference, modelSnapshot);
|
|
48
75
|
if (!record) return [{ value: 'auto', label: 'Automatic', hint: 'runtime/provider default' }];
|
|
49
76
|
return record.reasoning_efforts.map((value) => ({
|
|
50
77
|
value,
|
|
@@ -53,8 +80,8 @@ function reasoningOptions(modelPreference) {
|
|
|
53
80
|
}));
|
|
54
81
|
}
|
|
55
82
|
|
|
56
|
-
function serviceTierOptions(modelPreference) {
|
|
57
|
-
const record =
|
|
83
|
+
function serviceTierOptions(modelPreference, modelSnapshot) {
|
|
84
|
+
const record = resolvedModel(modelPreference, modelSnapshot);
|
|
58
85
|
if (!record) return [{ value: 'default', label: 'Standard', hint: 'default provider processing' }];
|
|
59
86
|
return record.service_tiers.map((value) => {
|
|
60
87
|
if (value === 'fast') return { value, label: 'Fast', hint: 'lower latency · 2× applicable token rates for Astra' };
|
|
@@ -68,20 +95,22 @@ async function promptModelPreferences(identity, existing, options = {}) {
|
|
|
68
95
|
try { status = await collectModelsStatus(options.modelStatusOptions || {}); } catch { /* inventory remains best-effort */ }
|
|
69
96
|
const models = modelOptions(status);
|
|
70
97
|
const initialModel = models.some((row) => row.value === existing.modelPreference) ? existing.modelPreference : 'auto';
|
|
71
|
-
const modelPreference = stopIfCancelled(await select({ message: 'Model', initialValue: initialModel, options: models }));
|
|
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);
|
|
72
101
|
|
|
73
|
-
const reasoning = reasoningOptions(modelPreference);
|
|
102
|
+
const reasoning = reasoningOptions(modelPreference, modelSnapshot);
|
|
74
103
|
const initialReasoning = reasoning.some((row) => row.value === existing.reasoningEffort) ? existing.reasoningEffort : reasoning[0].value;
|
|
75
104
|
const reasoningEffort = stopIfCancelled(await select({ message: 'Reasoning level', initialValue: initialReasoning, options: reasoning }));
|
|
76
105
|
|
|
77
|
-
const tiers = serviceTierOptions(modelPreference);
|
|
106
|
+
const tiers = serviceTierOptions(modelPreference, modelSnapshot);
|
|
78
107
|
const initialTier = tiers.some((row) => row.value === existing.serviceTier) ? existing.serviceTier : tiers[0].value;
|
|
79
108
|
const serviceTier = stopIfCancelled(await select({ message: 'Processing', initialValue: initialTier, options: tiers }));
|
|
80
109
|
|
|
81
|
-
return { modelPreference, reasoningEffort, serviceTier };
|
|
110
|
+
return { modelPreference, modelSnapshot, reasoningEffort, serviceTier };
|
|
82
111
|
}
|
|
83
112
|
|
|
84
|
-
export async function configureCliPreferences({ cwd = process.cwd(), firstRun = false, section = 'all', modelStatusOptions } = {}) {
|
|
113
|
+
export async function configureCliPreferences({ cwd = process.cwd(), firstRun = false, section = 'all', modelStatusOptions, home, env = process.env } = {}) {
|
|
85
114
|
let identity = detectCliProject(cwd);
|
|
86
115
|
const existing = readCliPreferences(identity.root) || {};
|
|
87
116
|
intro(firstRun ? `You are in ${identity.root}` : section === 'model' ? 'Agent Sam model' : 'Agent Sam settings');
|
|
@@ -100,8 +129,10 @@ export async function configureCliPreferences({ cwd = process.cwd(), firstRun =
|
|
|
100
129
|
}
|
|
101
130
|
}
|
|
102
131
|
|
|
103
|
-
|
|
104
|
-
|
|
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];
|
|
105
136
|
if (!firstRun && section === 'all') {
|
|
106
137
|
const projectChoice = stopIfCancelled(await select({
|
|
107
138
|
message: 'Where should Agent Sam work?',
|
|
@@ -124,20 +155,16 @@ export async function configureCliPreferences({ cwd = process.cwd(), firstRun =
|
|
|
124
155
|
}
|
|
125
156
|
runtime = stopIfCancelled(await select({
|
|
126
157
|
message: 'Runtime', initialValue: runtime,
|
|
127
|
-
options:
|
|
128
|
-
{ value: 'local', label: 'Local machine', hint: 'real shell + filesystem' },
|
|
129
|
-
{ value: 'remote', label: 'Remote', hint: 'connected remote runtime' },
|
|
130
|
-
{ value: 'sandbox', label: 'Sandbox', hint: 'isolated disposable runtime' },
|
|
131
|
-
],
|
|
158
|
+
options: runtimes,
|
|
132
159
|
}));
|
|
133
|
-
const shells = availableShells();
|
|
160
|
+
const shells = availableShells(env);
|
|
134
161
|
terminal = stopIfCancelled(await select({
|
|
135
162
|
message: 'Terminal', initialValue: terminal && shells.includes(terminal) ? terminal : shells[0],
|
|
136
163
|
options: shells.map((shell) => ({ value: shell, label: shell, hint: shell === shells[0] ? 'detected/default' : undefined })),
|
|
137
164
|
}));
|
|
138
165
|
}
|
|
139
166
|
|
|
140
|
-
const model = await promptModelPreferences(identity, existing, { modelStatusOptions });
|
|
167
|
+
const model = await promptModelPreferences(identity, existing, { modelStatusOptions: { home, env, ...(modelStatusOptions || {}) } });
|
|
141
168
|
const preferences = writeCliPreferences(identity.root, { trustedDirectory, runtime, terminal, ...model });
|
|
142
169
|
outro(`Ready · ${identity.project}`);
|
|
143
170
|
return { identity, preferences };
|