@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.
- package/AGENTSAM.md +55 -0
- package/README.md +12 -8
- package/bin/agentsam +2 -0
- package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
- package/docs/CLI_SHELL.md +163 -53
- package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
- package/docs/RELEASES.md +16 -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 +45 -12
- 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/connectors/cloudflare/package.json +10 -0
- package/packages/connectors/cloudflare/src/index.js +127 -0
- package/packages/connectors/cloudflare/src/owner.js +76 -0
- package/packages/connectors/cloudflare/src/routes.js +223 -0
- package/packages/connectors/cloudflare/src/vault.js +80 -0
- package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
- package/packages/identity/package.json +2 -2
- package/packages/identity/src/contracts/auth-config.js +18 -7
- package/packages/identity/tests/auth-config.test.mjs +9 -5
- package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
- 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/README.md +1 -0
- package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
- package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
- package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
- package/protocol/capabilities/manifest.json +47 -0
- package/protocol/context/context-budget.schema.json +10 -15
- package/protocol/context/context-item.schema.json +4 -5
- package/protocol/context/resolved-context-pack.schema.json +19 -14
- package/protocol/models/README.md +373 -0
- package/protocol/models/model-inventory-v2.schema.json +212 -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/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
- package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
- package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
- package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
- package/skills/catalog.json +18 -0
- package/src/agent/capability-adapter.js +25 -13
- package/src/agent/index.js +1 -0
- package/src/agent/responses-runner.js +353 -0
- package/src/capabilities/repository-snapshot.js +3 -3
- package/src/cli.js +118 -31
- package/src/cloudflare/cpu-profile.js +115 -0
- package/src/cloudflare/index.js +14 -0
- package/src/cloudflare/wrangler.js +132 -0
- package/src/commands/account-auth.js +47 -0
- package/src/commands/cloudflare.js +58 -0
- package/src/commands/connections.js +93 -0
- package/src/commands/context-economics.js +129 -0
- package/src/commands/context.js +1 -1
- package/src/commands/db.js +20 -3
- package/src/commands/deploy.js +39 -3
- package/src/commands/env.js +90 -0
- package/src/commands/eval.js +63 -0
- package/src/commands/interactive.js +2 -5
- 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 +149 -46
- package/src/commands/ollama.js +26 -0
- package/src/commands/preferences.js +130 -61
- package/src/commands/resume.js +67 -0
- package/src/commands/security.js +5 -3
- package/src/commands/shell.js +568 -119
- package/src/commands/tunnel.js +2 -2
- package/src/commands/whoami.js +86 -0
- package/src/context/budget.js +68 -6
- package/src/context/index.js +3 -1
- package/src/context/rehydrate.js +35 -0
- package/src/context/resolve.js +44 -12
- package/src/errors/contract.js +236 -0
- package/src/errors/diagnostic.js +160 -0
- package/src/errors/index.js +23 -0
- package/src/eval/context.js +191 -0
- package/src/eval/index.js +1 -0
- package/src/index.js +68 -2
- 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/account-session.js +98 -0
- package/src/lib/agent-instructions.js +73 -0
- package/src/lib/auth.js +4 -0
- package/src/lib/cli-preferences.js +55 -24
- package/src/lib/deploy/git-guard.js +69 -0
- package/src/lib/deploy/health.js +57 -0
- package/src/lib/deploy/local-studio.js +283 -0
- package/src/lib/deploy/secret-scan.js +65 -0
- package/src/lib/deploy-receipt/index.js +2 -2
- package/src/lib/detect-context.js +2 -2
- package/src/lib/execution-approvals.js +59 -0
- package/src/lib/knowledge-docker.js +6 -3
- package/src/lib/local-sessions.js +148 -0
- package/src/lib/local-status.js +1 -1
- package/src/lib/project-config.js +1 -1
- package/src/lib/provider-credentials.js +183 -0
- package/src/lib/scaffold/templates/worker-api/index.js +101 -20
- package/src/lib/scaffold/wizards/worker-api.js +27 -11
- package/src/lib/slash-commands.js +23 -16
- 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/catalog.js +135 -0
- package/src/models/discovery.js +292 -0
- package/src/models/index.js +7 -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 +10 -0
- package/src/providers/ollama-chat.js +148 -0
- package/src/providers/openai-responses.js +426 -0
- 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/process.js +35 -9
- package/src/security/trust-boundary.js +2 -2
- package/src/telemetry/contracts.js +203 -0
- package/src/telemetry/events.js +51 -0
- package/src/telemetry/index.js +8 -0
- package/src/tools/hydrate.js +35 -0
- package/src/tools/index.js +1 -0
- package/src/ui/boot.js +15 -17
- 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/account-session.test.mjs +36 -0
- package/test/cli/preferences-runtime.test.mjs +11 -0
- package/test/cli/runtime-ui.test.mjs +74 -0
- package/test/cli-preferences.test.mjs +26 -5
- package/test/cloudflare-connector.test.mjs +96 -0
- package/test/cloudflare-runtime.test.mjs +75 -0
- package/test/context.test.mjs +61 -12
- package/test/deploy-health-scan.test.mjs +67 -0
- package/test/error-diagnostics.test.mjs +115 -0
- package/test/eval-context.test.mjs +37 -0
- package/test/execution-approvals.test.mjs +27 -0
- 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 +48 -0
- package/test/local-studio-deploy.test.mjs +83 -0
- package/test/model-catalog.test.mjs +43 -0
- package/test/models.test.mjs +127 -16
- package/test/npm10-lock.test.mjs +29 -0
- package/test/ollama.test.mjs +21 -0
- package/test/openai-responses.test.mjs +95 -0
- package/test/portable-context.test.mjs +1 -1
- package/test/provider-credentials.test.mjs +96 -0
- package/test/rehydrate.test.mjs +25 -0
- package/test/release-hygiene.test.mjs +13 -5
- package/test/responses-runner.test.mjs +150 -0
- package/test/shell.test.mjs +92 -23
- package/test/smoke.mjs +4 -1
- package/test/telemetry.test.mjs +79 -0
- package/test/terminal/local-pty.mock.test.mjs +151 -0
- package/test/tools-search.test.mjs +14 -1
- package/test/whoami-resume.test.mjs +56 -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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const PROVIDER_CREDENTIALS = Object.freeze({
|
|
6
|
+
openai: Object.freeze({ env: 'OPENAI_API_KEY', files: ['openai.env'] }),
|
|
7
|
+
gemini: Object.freeze({ env: 'GEMINI_API_KEY', files: ['gemini.env'] }),
|
|
8
|
+
anthropic: Object.freeze({ env: 'ANTHROPIC_API_KEY', files: ['anthropic.env'] }),
|
|
9
|
+
grok: Object.freeze({ env: 'XAI_API_KEY', files: ['grok.env', 'xai.env'] }),
|
|
10
|
+
cloudflare: Object.freeze({ env: 'CLOUDFLARE_API_TOKEN', files: ['cloudflare.env'], accountEnv: ['ACCOUNT_ID', 'CLOUDFLARE_ACCOUNT_ID'] }),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
14
|
+
function normalizeCloudflareAccountId(value) {
|
|
15
|
+
const id = clean(value);
|
|
16
|
+
if (!id) return '';
|
|
17
|
+
if (!/^[a-f0-9]{32}$/i.test(id)) throw new Error('invalid_cloudflare_account_id');
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function homeDirectory(options = {}) {
|
|
22
|
+
return path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseEnvValue(source, variable) {
|
|
26
|
+
for (const rawLine of String(source || '').split(/\r?\n/)) {
|
|
27
|
+
const line = rawLine.trim();
|
|
28
|
+
if (!line || line.startsWith('#')) continue;
|
|
29
|
+
const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
30
|
+
if (!match || match[1] !== variable) continue;
|
|
31
|
+
let value = match[2].trim();
|
|
32
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function secureFile(filename) {
|
|
39
|
+
const stat = fs.statSync(filename);
|
|
40
|
+
if (!stat.isFile()) return { ok: false, error: 'not_a_file' };
|
|
41
|
+
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) return { ok: false, error: 'permissions_too_open' };
|
|
42
|
+
return { ok: true, mode: stat.mode & 0o777 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function firstEnvValue(source, names = []) {
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
const value = clean(parseEnvValue(source, name));
|
|
48
|
+
if (value) return value;
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function firstRuntimeValue(env, names = []) {
|
|
54
|
+
for (const name of names) {
|
|
55
|
+
const value = clean(env?.[name]);
|
|
56
|
+
if (value) return value;
|
|
57
|
+
}
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function agentEnvDirectory(options = {}) {
|
|
62
|
+
return path.join(homeDirectory(options), '.agentsam', 'env.d');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function agentEnvLoaderPath(options = {}) {
|
|
66
|
+
return path.join(homeDirectory(options), '.agentsam', 'load-agent-env.sh');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ensureAgentEnvLoader(options = {}) {
|
|
70
|
+
const filename = agentEnvLoaderPath(options);
|
|
71
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true, mode: 0o700 });
|
|
72
|
+
const source = `# AgentSam provider environment loader. Source this file; do not execute it.
|
|
73
|
+
_agentsam_profile=\"\${1:-}\"
|
|
74
|
+
case \"\$_agentsam_profile\" in
|
|
75
|
+
openai|anthropic|gemini|grok|cloudflare) ;;
|
|
76
|
+
*) echo \"usage: source ~/.agentsam/load-agent-env.sh <openai|anthropic|gemini|grok|cloudflare>\" >&2; return 2 2>/dev/null || exit 2 ;;
|
|
77
|
+
esac
|
|
78
|
+
_agentsam_file=\"\${HOME}/.agentsam/env.d/\${_agentsam_profile}.env\"
|
|
79
|
+
if [ ! -f \"\$_agentsam_file\" ]; then
|
|
80
|
+
echo \"AgentSam provider profile not found: \$_agentsam_file\" >&2
|
|
81
|
+
return 1 2>/dev/null || exit 1
|
|
82
|
+
fi
|
|
83
|
+
set -a
|
|
84
|
+
. \"\$_agentsam_file\"
|
|
85
|
+
set +a
|
|
86
|
+
if [ \"\$_agentsam_profile\" = cloudflare ]; then
|
|
87
|
+
if [ -z \"\${ACCOUNT_ID:-}\" ] && [ -n \"\${CLOUDFLARE_ACCOUNT_ID:-}\" ]; then export ACCOUNT_ID=\"\$CLOUDFLARE_ACCOUNT_ID\"; fi
|
|
88
|
+
if [ -z \"\${CLOUDFLARE_ACCOUNT_ID:-}\" ] && [ -n \"\${ACCOUNT_ID:-}\" ]; then export CLOUDFLARE_ACCOUNT_ID=\"\$ACCOUNT_ID\"; fi
|
|
89
|
+
fi
|
|
90
|
+
unset _agentsam_file _agentsam_profile
|
|
91
|
+
`;
|
|
92
|
+
fs.writeFileSync(filename, source, { mode: 0o700 });
|
|
93
|
+
if (process.platform !== 'win32') fs.chmodSync(filename, 0o700);
|
|
94
|
+
return filename;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function profileTemplate(provider, options = {}) {
|
|
98
|
+
if (provider === 'cloudflare') {
|
|
99
|
+
const accountId = normalizeCloudflareAccountId(options.accountId);
|
|
100
|
+
return `# AgentSam Cloudflare profile\n# ACCOUNT_ID is your Cloudflare account identifier; it is not a secret.\nexport ACCOUNT_ID=\"${accountId}\"\nexport CLOUDFLARE_API_TOKEN=\"\"\n`;
|
|
101
|
+
}
|
|
102
|
+
const spec = PROVIDER_CREDENTIALS[provider];
|
|
103
|
+
if (!spec) throw new Error(`unsupported_provider:${provider}`);
|
|
104
|
+
return `# AgentSam ${provider} provider profile\nexport ${spec.env}=\"\"\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function ensureProviderEnvProfile(provider, options = {}) {
|
|
108
|
+
const spec = providerCredentialSpec(provider);
|
|
109
|
+
if (!spec) throw new Error(`unsupported_provider:${clean(provider).toLowerCase()}`);
|
|
110
|
+
const dir = agentEnvDirectory(options);
|
|
111
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
112
|
+
if (process.platform !== 'win32') fs.chmodSync(dir, 0o700);
|
|
113
|
+
const filename = path.join(dir, spec.files[0]);
|
|
114
|
+
let created = false;
|
|
115
|
+
if (!fs.existsSync(filename)) {
|
|
116
|
+
fs.writeFileSync(filename, profileTemplate(spec.provider, options), { mode: 0o600 });
|
|
117
|
+
created = true;
|
|
118
|
+
} else if (spec.provider === 'cloudflare' && clean(options.accountId)) {
|
|
119
|
+
const source = fs.readFileSync(filename, 'utf8');
|
|
120
|
+
const current = firstEnvValue(source, spec.accountEnv || []);
|
|
121
|
+
if (!current) {
|
|
122
|
+
const accountId = normalizeCloudflareAccountId(options.accountId);
|
|
123
|
+
const line = `export ACCOUNT_ID=\"${accountId}\"`;
|
|
124
|
+
const next = /^(?:export\s+)?ACCOUNT_ID=.*$/m.test(source)
|
|
125
|
+
? source.replace(/^(?:export\s+)?ACCOUNT_ID=.*$/m, line)
|
|
126
|
+
: `${line}\n${source}`;
|
|
127
|
+
fs.writeFileSync(filename, next, { mode: 0o600 });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (process.platform !== 'win32') fs.chmodSync(filename, 0o600);
|
|
131
|
+
const loader = ensureAgentEnvLoader(options);
|
|
132
|
+
return Object.freeze({ provider: spec.provider, file: filename, loader, created, source_command: `source ~/.agentsam/load-agent-env.sh ${spec.provider}` });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function providerCredentialSpec(provider) {
|
|
136
|
+
const id = clean(provider).toLowerCase();
|
|
137
|
+
const spec = PROVIDER_CREDENTIALS[id];
|
|
138
|
+
return spec ? Object.freeze({ provider: id, ...spec }) : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resolveProviderCredential(provider, options = {}) {
|
|
142
|
+
const spec = providerCredentialSpec(provider);
|
|
143
|
+
if (!spec) return Object.freeze({ provider: clean(provider).toLowerCase(), configured: false, source: null, error: 'unsupported_provider', value: '' });
|
|
144
|
+
const env = options.env || process.env;
|
|
145
|
+
const accountIdFromEnv = firstRuntimeValue(env, spec.accountEnv || []);
|
|
146
|
+
const fromEnv = clean(env?.[spec.env]);
|
|
147
|
+
if (fromEnv) return Object.freeze({ provider: spec.provider, configured: true, source: 'environment', env: spec.env, file: null, error: null, value: fromEnv, account_id: accountIdFromEnv || null });
|
|
148
|
+
|
|
149
|
+
const dir = agentEnvDirectory({ ...options, env });
|
|
150
|
+
for (const basename of spec.files) {
|
|
151
|
+
const filename = path.join(dir, basename);
|
|
152
|
+
if (!fs.existsSync(filename)) continue;
|
|
153
|
+
try {
|
|
154
|
+
const safety = secureFile(filename);
|
|
155
|
+
if (!safety.ok) return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: safety.error, value: '' });
|
|
156
|
+
const source = fs.readFileSync(filename, 'utf8');
|
|
157
|
+
const value = clean(parseEnvValue(source, spec.env));
|
|
158
|
+
const accountId = accountIdFromEnv || firstEnvValue(source, spec.accountEnv || []);
|
|
159
|
+
if (value) return Object.freeze({ provider: spec.provider, configured: true, source: 'agentsam_env_file', env: spec.env, file: filename, error: null, value, account_id: accountId || null });
|
|
160
|
+
return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: 'credential_variable_missing', value: '' });
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: error?.message || String(error), value: '' });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return Object.freeze({ provider: spec.provider, configured: false, source: null, env: spec.env, file: null, error: null, value: '' });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function describeProviderCredential(provider, options = {}) {
|
|
169
|
+
const resolved = resolveProviderCredential(provider, options);
|
|
170
|
+
return Object.freeze({
|
|
171
|
+
provider: resolved.provider,
|
|
172
|
+
configured: resolved.configured,
|
|
173
|
+
source: resolved.source,
|
|
174
|
+
env: resolved.env || null,
|
|
175
|
+
file: resolved.file || null,
|
|
176
|
+
error: resolved.error || null,
|
|
177
|
+
account_id: resolved.account_id || null,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function listProviderCredentialStatus(options = {}) {
|
|
182
|
+
return Object.freeze(Object.keys(PROVIDER_CREDENTIALS).map((provider) => describeProviderCredential(provider, options)));
|
|
183
|
+
}
|
|
@@ -2,31 +2,36 @@
|
|
|
2
2
|
* Worker API template generator.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export function workerApiTemplates({ projectName, routes, cfAccountId }) {
|
|
5
|
+
export function workerApiTemplates({ projectName, routes, cfAccountId, dbKind = 'd1' }) {
|
|
6
6
|
const files = {};
|
|
7
7
|
|
|
8
|
+
const pkgScripts = {
|
|
9
|
+
deploy: 'wrangler deploy',
|
|
10
|
+
dev: 'wrangler dev',
|
|
11
|
+
...(dbKind === 'hyperdrive'
|
|
12
|
+
? { 'db:migrate': 'psql "$DATABASE_URL" -f migrations/001_init.sql' }
|
|
13
|
+
: { 'db:migrate': `wrangler d1 execute ${projectName} --file=migrations/001_init.sql --remote` }),
|
|
14
|
+
};
|
|
15
|
+
|
|
8
16
|
files['package.json'] = JSON.stringify({
|
|
9
17
|
name: projectName,
|
|
10
18
|
version: '0.1.0',
|
|
11
19
|
private: true,
|
|
12
|
-
scripts:
|
|
13
|
-
deploy: 'wrangler deploy',
|
|
14
|
-
dev: 'wrangler dev',
|
|
15
|
-
'db:migrate': `wrangler d1 execute ${projectName} --file=migrations/001_init.sql --remote`,
|
|
16
|
-
},
|
|
20
|
+
scripts: pkgScripts,
|
|
17
21
|
devDependencies: { wrangler: '^3.0.0' },
|
|
22
|
+
...(dbKind === 'hyperdrive' ? { dependencies: { postgres: '^3.4.0' } } : {}),
|
|
18
23
|
}, null, 2);
|
|
19
24
|
|
|
25
|
+
const dbBindingBlock = dbKind === 'hyperdrive'
|
|
26
|
+
? `[[hyperdrive]]\nbinding = "HYPERDRIVE"\nid = "REPLACE_WITH_YOUR_HYPERDRIVE_ID"\n`
|
|
27
|
+
: `[[d1_databases]]\nbinding = "DB"\ndatabase_name = "${projectName}"\ndatabase_id = "REPLACE_WITH_YOUR_D1_ID"\n`;
|
|
28
|
+
|
|
20
29
|
files['wrangler.toml'] = `name = "${projectName}"
|
|
21
30
|
main = "src/index.js"
|
|
22
31
|
compatibility_date = "2024-01-01"
|
|
23
32
|
account_id = "${cfAccountId}"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
binding = "DB"
|
|
27
|
-
database_name = "${projectName}"
|
|
28
|
-
database_id = "REPLACE_WITH_YOUR_D1_ID"
|
|
29
|
-
`;
|
|
33
|
+
${dbKind === 'hyperdrive' ? 'compatibility_flags = ["nodejs_compat"]\n' : ''}
|
|
34
|
+
${dbBindingBlock}`;
|
|
30
35
|
|
|
31
36
|
// Entry
|
|
32
37
|
const routeImports = routes.map(r => `import { handle${cap(r)} } from './routes/${r}.js';`).join('\n');
|
|
@@ -83,6 +88,37 @@ ${routeMatches}
|
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
if (routes.includes('users')) {
|
|
91
|
+
if (dbKind === 'hyperdrive') {
|
|
92
|
+
files['src/routes/users.js'] = `import postgres from 'postgres';
|
|
93
|
+
|
|
94
|
+
export async function handleUsers(request, env) {
|
|
95
|
+
const sql = postgres(env.HYPERDRIVE.connectionString);
|
|
96
|
+
const url = new URL(request.url);
|
|
97
|
+
const id = url.pathname.replace('/users/', '').replace('/users', '') || null;
|
|
98
|
+
|
|
99
|
+
if (request.method === 'GET' && !id) {
|
|
100
|
+
const rows = await sql\`SELECT * FROM users LIMIT 50\`;
|
|
101
|
+
return Response.json(rows);
|
|
102
|
+
}
|
|
103
|
+
if (request.method === 'GET' && id) {
|
|
104
|
+
const rows = await sql\`SELECT * FROM users WHERE id = \${id}\`;
|
|
105
|
+
if (!rows[0]) return Response.json({ error: 'Not found' }, { status: 404 });
|
|
106
|
+
return Response.json(rows[0]);
|
|
107
|
+
}
|
|
108
|
+
if (request.method === 'POST') {
|
|
109
|
+
const body = await request.json();
|
|
110
|
+
const newId = crypto.randomUUID();
|
|
111
|
+
await sql\`INSERT INTO users (id, email) VALUES (\${newId}, \${body.email})\`;
|
|
112
|
+
return Response.json({ id: newId }, { status: 201 });
|
|
113
|
+
}
|
|
114
|
+
if (request.method === 'DELETE' && id) {
|
|
115
|
+
await sql\`DELETE FROM users WHERE id = \${id}\`;
|
|
116
|
+
return Response.json({ ok: true });
|
|
117
|
+
}
|
|
118
|
+
return Response.json({ error: 'Method not allowed' }, { status: 405 });
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
} else {
|
|
86
122
|
files['src/routes/users.js'] = `export async function handleUsers(request, env) {
|
|
87
123
|
const url = new URL(request.url);
|
|
88
124
|
const id = url.pathname.replace('/users/', '').replace('/users', '') || null;
|
|
@@ -109,9 +145,38 @@ ${routeMatches}
|
|
|
109
145
|
return Response.json({ error: 'Method not allowed' }, { status: 405 });
|
|
110
146
|
}
|
|
111
147
|
`;
|
|
148
|
+
}
|
|
112
149
|
}
|
|
113
150
|
|
|
114
151
|
if (routes.includes('content')) {
|
|
152
|
+
if (dbKind === 'hyperdrive') {
|
|
153
|
+
files['src/routes/content.js'] = `import postgres from 'postgres';
|
|
154
|
+
|
|
155
|
+
export async function handleContent(request, env) {
|
|
156
|
+
const sql = postgres(env.HYPERDRIVE.connectionString);
|
|
157
|
+
const url = new URL(request.url);
|
|
158
|
+
const slug = url.pathname.replace('/content/', '').replace('/content', '') || null;
|
|
159
|
+
|
|
160
|
+
if (request.method === 'GET' && !slug) {
|
|
161
|
+
const rows = await sql\`SELECT id, slug, title, status FROM cms_pages LIMIT 50\`;
|
|
162
|
+
return Response.json(rows);
|
|
163
|
+
}
|
|
164
|
+
if (request.method === 'GET' && slug) {
|
|
165
|
+
const rows = await sql\`SELECT * FROM cms_pages WHERE slug = \${slug}\`;
|
|
166
|
+
if (!rows[0]) return Response.json({ error: 'Not found' }, { status: 404 });
|
|
167
|
+
return Response.json(rows[0]);
|
|
168
|
+
}
|
|
169
|
+
if (request.method === 'POST') {
|
|
170
|
+
const body = await request.json();
|
|
171
|
+
const id = crypto.randomUUID();
|
|
172
|
+
await sql\`INSERT INTO cms_pages (id, slug, title, template, content_json)
|
|
173
|
+
VALUES (\${id}, \${body.slug}, \${body.title}, \${body.template ?? 'default'}, \${JSON.stringify(body.content ?? {})})\`;
|
|
174
|
+
return Response.json({ id }, { status: 201 });
|
|
175
|
+
}
|
|
176
|
+
return Response.json({ error: 'Method not allowed' }, { status: 405 });
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
179
|
+
} else {
|
|
115
180
|
files['src/routes/content.js'] = `export async function handleContent(request, env) {
|
|
116
181
|
const url = new URL(request.url);
|
|
117
182
|
const slug = url.pathname.replace('/content/', '').replace('/content', '') || null;
|
|
@@ -136,6 +201,7 @@ ${routeMatches}
|
|
|
136
201
|
return Response.json({ error: 'Method not allowed' }, { status: 405 });
|
|
137
202
|
}
|
|
138
203
|
`;
|
|
204
|
+
}
|
|
139
205
|
}
|
|
140
206
|
|
|
141
207
|
if (routes.includes('webhook')) {
|
|
@@ -154,13 +220,17 @@ ${routeMatches}
|
|
|
154
220
|
`;
|
|
155
221
|
}
|
|
156
222
|
|
|
157
|
-
// Migration
|
|
223
|
+
// Migration — column syntax differs: SQLite (D1) uses INTEGER/unixepoch(), Postgres uses TIMESTAMPTZ/NOW()
|
|
224
|
+
const createdAtCol = dbKind === 'hyperdrive'
|
|
225
|
+
? 'created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()'
|
|
226
|
+
: 'created_at INTEGER NOT NULL DEFAULT (unixepoch())';
|
|
227
|
+
|
|
158
228
|
let sql = `-- ${projectName} initial schema\n\n`;
|
|
159
229
|
if (routes.includes('users')) {
|
|
160
230
|
sql += `CREATE TABLE IF NOT EXISTS users (
|
|
161
231
|
id TEXT PRIMARY KEY,
|
|
162
232
|
email TEXT NOT NULL UNIQUE,
|
|
163
|
-
|
|
233
|
+
${createdAtCol}
|
|
164
234
|
);\n\n`;
|
|
165
235
|
}
|
|
166
236
|
if (routes.includes('content')) {
|
|
@@ -171,12 +241,23 @@ ${routeMatches}
|
|
|
171
241
|
template TEXT NOT NULL DEFAULT 'default',
|
|
172
242
|
content_json TEXT,
|
|
173
243
|
status TEXT NOT NULL DEFAULT 'draft',
|
|
174
|
-
|
|
244
|
+
${createdAtCol}
|
|
175
245
|
);\n\n`;
|
|
176
246
|
}
|
|
177
247
|
|
|
178
248
|
files['migrations/001_init.sql'] = sql;
|
|
179
249
|
|
|
250
|
+
const deploySteps = dbKind === 'hyperdrive'
|
|
251
|
+
? `npm install
|
|
252
|
+
npx wrangler hyperdrive create ${projectName} --connection-string="postgres://user:pass@host:5432/db"
|
|
253
|
+
# paste the returned Hyperdrive id into wrangler.toml as HYPERDRIVE binding
|
|
254
|
+
npx wrangler deploy`
|
|
255
|
+
: `npm install
|
|
256
|
+
npx wrangler d1 create ${projectName}
|
|
257
|
+
# paste database_id into wrangler.toml
|
|
258
|
+
npx wrangler d1 execute ${projectName} --file=migrations/001_init.sql --remote
|
|
259
|
+
npx wrangler deploy`;
|
|
260
|
+
|
|
180
261
|
files['README.md'] = `# ${projectName}
|
|
181
262
|
|
|
182
263
|
Scaffolded by [@inneranimalmedia/agentsam-sdk](https://github.com/SamPrimeaux/agentsam-sdk).
|
|
@@ -185,14 +266,14 @@ Scaffolded by [@inneranimalmedia/agentsam-sdk](https://github.com/SamPrimeaux/ag
|
|
|
185
266
|
|
|
186
267
|
${routes.map(r => `- \`/${r}\``).join('\n')}
|
|
187
268
|
|
|
269
|
+
## Database
|
|
270
|
+
|
|
271
|
+
${dbKind === 'hyperdrive' ? 'Your own Postgres, via Cloudflare Hyperdrive.' : 'Cloudflare D1 (SQLite).'}
|
|
272
|
+
|
|
188
273
|
## Deploy
|
|
189
274
|
|
|
190
275
|
\`\`\`bash
|
|
191
|
-
|
|
192
|
-
npx wrangler d1 create ${projectName}
|
|
193
|
-
# paste database_id into wrangler.toml
|
|
194
|
-
npx wrangler d1 execute ${projectName} --file=migrations/001_init.sql --remote
|
|
195
|
-
npx wrangler deploy
|
|
276
|
+
${deploySteps}
|
|
196
277
|
\`\`\`
|
|
197
278
|
`;
|
|
198
279
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
text,
|
|
8
|
+
select,
|
|
8
9
|
multiselect,
|
|
9
10
|
confirm,
|
|
10
11
|
spinner,
|
|
@@ -42,6 +43,15 @@ export async function runWorkerApiWizard() {
|
|
|
42
43
|
});
|
|
43
44
|
if (isCancel(routes)) { cancel('Cancelled.'); process.exit(0); }
|
|
44
45
|
|
|
46
|
+
const dbKind = await select({
|
|
47
|
+
message: 'Database?',
|
|
48
|
+
options: [
|
|
49
|
+
{ value: 'd1', label: 'Cloudflare D1', hint: 'Native, free tier, SQLite' },
|
|
50
|
+
{ value: 'hyperdrive', label: 'Your own Postgres (Hyperdrive)', hint: 'Supabase, Neon, RDS, etc.' },
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
if (isCancel(dbKind)) { cancel('Cancelled.'); process.exit(0); }
|
|
54
|
+
|
|
45
55
|
const cfAccountId = await text({
|
|
46
56
|
message: 'Cloudflare account ID?',
|
|
47
57
|
placeholder: 'abc123...',
|
|
@@ -57,20 +67,26 @@ export async function runWorkerApiWizard() {
|
|
|
57
67
|
const s = spinner();
|
|
58
68
|
s.start('Generating files...');
|
|
59
69
|
|
|
60
|
-
const config = { projectName: projectName.trim(), routes, cfAccountId: cfAccountId.trim() };
|
|
70
|
+
const config = { projectName: projectName.trim(), routes, cfAccountId: cfAccountId.trim(), dbKind };
|
|
61
71
|
const fileTree = workerApiTemplates(config);
|
|
62
72
|
await writeFileTree(`./${config.projectName}`, fileTree);
|
|
63
73
|
|
|
64
74
|
s.stop(pc.green(`Files written to ./${config.projectName}/`));
|
|
65
75
|
|
|
66
|
-
|
|
67
|
-
[
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
const nextSteps = dbKind === 'hyperdrive'
|
|
77
|
+
? [
|
|
78
|
+
`cd ${config.projectName}`,
|
|
79
|
+
`npm install`,
|
|
80
|
+
`npx wrangler hyperdrive create ${config.projectName} --connection-string="postgres://user:pass@host:5432/db"`,
|
|
81
|
+
`# paste the returned Hyperdrive id into wrangler.toml`,
|
|
82
|
+
`npx wrangler deploy`,
|
|
83
|
+
]
|
|
84
|
+
: [
|
|
85
|
+
`cd ${config.projectName}`,
|
|
86
|
+
`npm install`,
|
|
87
|
+
`npx wrangler d1 create ${config.projectName}`,
|
|
88
|
+
`npx wrangler d1 execute ${config.projectName} --file=migrations/001_init.sql --remote`,
|
|
89
|
+
`npx wrangler deploy`,
|
|
90
|
+
];
|
|
91
|
+
note(nextSteps.join('\n'), 'Next steps');
|
|
76
92
|
}
|
|
@@ -1,35 +1,42 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canonical slash-command surface for Agent Sam SDK CLI / shell UX.
|
|
3
|
-
* Consumed by the interactive `agentsam shell` REPL and presentation layers.
|
|
4
|
-
*/
|
|
1
|
+
/** Canonical, implemented slash-command surface for Agent Sam SDK CLI. */
|
|
5
2
|
|
|
6
3
|
export const SHELL_THEMES = ['NIGHT', 'DAY', 'LAVA', 'VOID'];
|
|
7
4
|
|
|
8
|
-
/** @type {Array<{ cmd: string, description: string, lane?: string }>} */
|
|
9
5
|
export const SLASH_COMMANDS = [
|
|
10
|
-
{ cmd: '/
|
|
6
|
+
{ cmd: '/model', description: 'Choose exact model, reasoning level, and processing tier', lane: 'model' },
|
|
7
|
+
{ cmd: '/reasoning', description: 'Set reasoning effort for the selected model', lane: 'model' },
|
|
8
|
+
{ cmd: '/fast', description: 'Use provider Fast processing when the selected model supports it', lane: 'model' },
|
|
9
|
+
{ cmd: '/flex', description: 'Use provider Flex processing when the selected model supports it', lane: 'model' },
|
|
10
|
+
{ cmd: '/standard', description: 'Return to Standard provider processing', lane: 'model' },
|
|
11
|
+
{ cmd: '/context', description: 'Show model context economics; add repo for Git bridge context', lane: 'context' },
|
|
11
12
|
{ cmd: '/status', description: 'Local project, DB, Git, and PTY health', lane: 'local' },
|
|
12
|
-
{ cmd: '/
|
|
13
|
+
{ cmd: '/models', description: 'Probe providers and provider-verified known models', lane: 'model' },
|
|
14
|
+
{ cmd: '/login', description: 'Sign in to Inner Animal Media and save the machine-local Agent Sam session', lane: 'identity' },
|
|
15
|
+
{ cmd: '/logout', description: 'Sign out locally without deleting provider credentials', lane: 'identity' },
|
|
16
|
+
{ cmd: '/whoami', description: 'Show authenticated account identity and safe credential status', lane: 'identity' },
|
|
17
|
+
{ cmd: '/session', description: 'Show current session usage, cost, and resume receipt', lane: 'observability' },
|
|
18
|
+
{ cmd: '/usage', description: 'Show token usage, spend breakdown, and resume command', lane: 'observability' },
|
|
19
|
+
{ cmd: '/cf', description: 'Cloudflare native reads, Wrangler status, and CPU profile analysis', lane: 'cloudflare' },
|
|
20
|
+
{ cmd: '/settings', description: 'Choose project, runtime, terminal, and model policy' },
|
|
13
21
|
{ cmd: '/pwd', description: 'Print working directory', lane: 'terminal' },
|
|
14
22
|
{ cmd: '/cd', description: 'Change working directory', lane: 'terminal' },
|
|
15
|
-
{ cmd: '/git', description: '
|
|
23
|
+
{ cmd: '/git', description: 'Run an explicit Git subcommand', lane: 'git' },
|
|
24
|
+
{ cmd: '/diff', description: 'Show the current Git diff', lane: 'git' },
|
|
16
25
|
{ cmd: '/db', description: 'Local SQLite status and query helpers', lane: 'data' },
|
|
17
|
-
{ cmd: '/agent', description: 'Send a goal to the configured Agent Sam', lane: 'agent' },
|
|
18
|
-
{ cmd: '/models', description: 'Show available model providers and local models' },
|
|
19
|
-
{ cmd: '/settings', description: 'Choose project, runtime, terminal, and model preference' },
|
|
26
|
+
{ cmd: '/agent', description: 'Send a goal to the configured local Agent Sam runtime', lane: 'agent' },
|
|
20
27
|
{ cmd: '/logs', description: 'Show local Agent Sam execution events', lane: 'observability' },
|
|
21
28
|
{ cmd: '/deploy', description: 'Add a cloud adapter and deploy intentionally', lane: 'deploy' },
|
|
29
|
+
{ cmd: '/clear', description: 'Clear the terminal display', lane: 'terminal' },
|
|
30
|
+
{ cmd: '/help', description: 'Show the factual implemented command catalog' },
|
|
22
31
|
{ cmd: '/exit', description: 'Exit Agent Sam shell and return to the host terminal' },
|
|
23
32
|
];
|
|
24
33
|
|
|
25
|
-
/** Shell UX rollout phases (gorilla-shell → SDK default CLI experience). */
|
|
26
34
|
export const SHELL_PHASES = [
|
|
27
35
|
{ id: '0-prototype', label: 'Visual prototype + demo scenarios', status: 'complete' },
|
|
28
|
-
{ id: 'pty-connection', label: 'Local PTY via agentsam start-local', status: '
|
|
29
|
-
{ id: '
|
|
30
|
-
{ id: '
|
|
36
|
+
{ id: 'pty-connection', label: 'Local PTY via agentsam start-local', status: 'complete' },
|
|
37
|
+
{ id: 'model-context-controls', label: 'Model, reasoning, processing, and context economics', status: 'complete' },
|
|
38
|
+
{ id: 'run-telemetry', label: 'Provider usage, cost, resumable sessions, and permission receipts', status: 'current' },
|
|
31
39
|
{ id: 'dashboard-embed', label: 'Embeddable shell for IAM dashboard', status: 'planned' },
|
|
32
|
-
{ id: 'standalone-pwa', label: 'Installable PWA / SDK default shell', status: 'planned' },
|
|
33
40
|
];
|
|
34
41
|
|
|
35
42
|
export function listSlashCommands(opts = {}) {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MIGRATIONS_DIR = path.resolve(
|
|
6
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
7
|
+
'..',
|
|
8
|
+
'..',
|
|
9
|
+
'migrations',
|
|
10
|
+
'runtime',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
function migrationFiles(dir) {
|
|
14
|
+
if (!fs.existsSync(dir)) return [];
|
|
15
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
16
|
+
.filter((entry) => entry.isFile() && /^\d+.*\.sql$/i.test(entry.name))
|
|
17
|
+
.map((entry) => entry.name)
|
|
18
|
+
.sort((a, b) => a.localeCompare(b));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureLedger(db) {
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS agentsam_schema_migrations (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
applied_at_unix INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
26
|
+
checksum TEXT
|
|
27
|
+
);
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checksumSource(source) {
|
|
32
|
+
let hash = 2166136261;
|
|
33
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
34
|
+
hash ^= source.charCodeAt(i);
|
|
35
|
+
hash = Math.imul(hash, 16777619);
|
|
36
|
+
}
|
|
37
|
+
return 'fnv1a32:' + (hash >>> 0).toString(16).padStart(8, '0');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function listAppliedMigrations(db) {
|
|
41
|
+
ensureLedger(db);
|
|
42
|
+
const result = await db.prepare(
|
|
43
|
+
'SELECT id, applied_at_unix, checksum FROM agentsam_schema_migrations ORDER BY id'
|
|
44
|
+
).all();
|
|
45
|
+
return result.results || [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function applyRuntimeMigrations(db, options = {}) {
|
|
49
|
+
const dir = path.resolve(options.migrationsDir || DEFAULT_MIGRATIONS_DIR);
|
|
50
|
+
ensureLedger(db);
|
|
51
|
+
const appliedRows = await listAppliedMigrations(db);
|
|
52
|
+
const applied = new Map(appliedRows.map((row) => [String(row.id), row]));
|
|
53
|
+
const results = [];
|
|
54
|
+
|
|
55
|
+
for (const filename of migrationFiles(dir)) {
|
|
56
|
+
const id = filename.replace(/\.sql$/i, '');
|
|
57
|
+
const source = fs.readFileSync(path.join(dir, filename), 'utf8');
|
|
58
|
+
const checksum = checksumSource(source);
|
|
59
|
+
const previous = applied.get(id);
|
|
60
|
+
|
|
61
|
+
if (previous) {
|
|
62
|
+
if (previous.checksum && previous.checksum !== checksum) {
|
|
63
|
+
throw new Error('migration_checksum_mismatch:' + id);
|
|
64
|
+
}
|
|
65
|
+
results.push({ id, filename, status: 'already_applied', checksum });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
db.exec('BEGIN IMMEDIATE');
|
|
70
|
+
try {
|
|
71
|
+
db.exec(source);
|
|
72
|
+
await db.prepare(
|
|
73
|
+
'INSERT INTO agentsam_schema_migrations (id, checksum) VALUES (?, ?)'
|
|
74
|
+
).bind(id, checksum).run();
|
|
75
|
+
db.exec('COMMIT');
|
|
76
|
+
results.push({ id, filename, status: 'applied', checksum });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
try { db.exec('ROLLBACK'); } catch {}
|
|
79
|
+
throw new Error('migration_failed:' + id + ':' + (error?.message || error));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
migrationsDir: dir,
|
|
85
|
+
total: results.length,
|
|
86
|
+
applied: results.filter((row) => row.status === 'applied').length,
|
|
87
|
+
results: Object.freeze(results),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function runtimeMigrationsDirectory() {
|
|
92
|
+
return DEFAULT_MIGRATIONS_DIR;
|
|
93
|
+
}
|