@inneranimalmedia/agentsam-sdk 2.4.1 → 2.6.0
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 +17 -12
- package/bin/agentsam +2 -0
- package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
- package/docs/AGENTSAM_WORKBENCH.md +30 -0
- package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
- package/docs/CAPABILITIES.md +5 -3
- package/docs/CLI_SHELL.md +167 -80
- package/docs/CMS_STUDIO.md +101 -0
- package/docs/CONTEXT.md +170 -0
- package/docs/LOCAL_OLLAMA.md +58 -0
- package/docs/MERKLE.md +19 -19
- package/docs/PORTABLE_CONTEXT.md +4 -3
- package/docs/PROJECT_CONFIG.md +72 -0
- package/docs/RELEASES.md +35 -2
- package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
- package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
- package/docs/SDK_WORKER.md +86 -0
- package/docs/SECURITY.md +59 -22
- package/docs/client-cms-editor.md +15 -0
- package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
- package/package.json +35 -10
- 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 +10 -1
- package/packages/identity/src/contracts/auth-config.js +109 -0
- package/packages/identity/src/index.js +1 -0
- package/packages/identity/src/oauth/README.md +2 -2
- package/packages/identity/src/oauth/credentials.js +11 -4
- package/packages/identity/src/oauth/iam-platform.js +3 -3
- package/packages/identity/src/providers/iam/index.js +7 -7
- package/packages/identity/src/providers/iam/oauth.js +6 -4
- package/packages/identity/src/providers/iam/profile.js +5 -5
- package/packages/identity/tests/auth-config.test.mjs +61 -0
- package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
- package/protocol/FILEMETA_V1.md +95 -0
- package/protocol/INSPECT_VIEWS_V1.md +40 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
- package/protocol/MERKLE_V1.md +3 -1
- 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 +79 -3
- package/protocol/capabilities/repository-snapshot.schema.json +1 -0
- package/protocol/context/context-budget.schema.json +36 -0
- package/protocol/context/context-item.schema.json +19 -0
- package/protocol/context/resolved-context-pack.schema.json +47 -0
- package/protocol/context/result-policy.schema.json +17 -0
- package/protocol/knowledge/context-pack.schema.json +33 -0
- package/protocol/knowledge/retrieval-query.schema.json +68 -13
- package/protocol/models/README.md +373 -0
- package/protocol/models/model-inventory-v2.schema.json +212 -0
- package/python/README.md +15 -7
- package/python/agentsam_sdk/cli.py +0 -21
- package/python/agentsam_sdk/tui/README.md +17 -12
- package/python/agentsam_sdk/tui/bootstrap.py +2 -2
- package/python/agentsam_sdk/tui/demo.py +25 -10
- package/python/agentsam_sdk/tui/onboarding.py +208 -0
- package/python/tests/test_tui_cli.py +7 -6
- package/skills/README.md +21 -0
- package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
- package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
- package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
- 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/agentsam-jr-dev/SKILL.md +232 -0
- package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
- package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
- package/skills/agentsam-progression-guard/SKILL.md +197 -0
- package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
- package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
- package/skills/catalog.json +71 -0
- package/src/agent/capability-adapter.js +25 -13
- package/src/agent/index.js +1 -0
- package/src/agent/responses-runner.js +325 -0
- package/src/capabilities/index.js +7 -0
- package/src/capabilities/repository-snapshot-view.js +238 -0
- package/src/capabilities/repository-snapshot.js +28 -14
- package/src/cli.js +198 -76
- 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 +114 -0
- package/src/commands/context.js +14 -2
- package/src/commands/db.js +4 -7
- package/src/commands/deploy.js +54 -32
- package/src/commands/eval.js +63 -0
- package/src/commands/interactive.js +18 -0
- package/src/commands/knowledge.js +27 -4
- package/src/commands/merkle-persist.js +118 -0
- package/src/commands/merkle.js +32 -17
- package/src/commands/models.js +152 -0
- package/src/commands/ollama.js +259 -0
- package/src/commands/preferences.js +144 -0
- package/src/commands/product.js +86 -16
- package/src/commands/resume.js +67 -0
- package/src/commands/security.js +8 -6
- package/src/commands/shell.js +405 -100
- package/src/commands/skills.js +66 -0
- package/src/commands/start-local.js +1 -1
- package/src/commands/tunnel.js +5 -4
- package/src/commands/whoami.js +86 -0
- package/src/context/budget.js +120 -0
- package/src/context/compact.js +28 -0
- package/src/context/index.js +7 -0
- package/src/context/rehydrate.js +35 -0
- package/src/context/resolve.js +116 -0
- package/src/context/result-policy.js +66 -0
- package/src/errors/diagnostic.js +160 -0
- package/src/errors/index.js +9 -0
- package/src/eval/context.js +191 -0
- package/src/eval/index.js +1 -0
- package/src/index.js +70 -0
- package/src/indexing/execution-boundary.js +144 -0
- package/src/indexing/index.js +8 -0
- package/src/indexing/provider.js +41 -0
- package/src/knowledge/config.js +2 -2
- package/src/knowledge/context-pack.js +12 -1
- package/src/knowledge/contracts.js +9 -4
- package/src/knowledge/engine.js +7 -3
- package/src/knowledge/service/server.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/bridge-client.js +7 -5
- package/src/lib/cli-preferences.js +78 -0
- package/src/lib/core-client.js +8 -8
- 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 +5 -2
- package/src/lib/detect-context.js +6 -5
- package/src/lib/execution-approvals.js +59 -0
- package/src/lib/identity-scaffold.js +1 -1
- package/src/lib/local-scaffold.js +35 -33
- package/src/lib/local-sessions.js +127 -0
- package/src/lib/local-status.js +9 -17
- package/src/lib/merkle/cloudflare-persistence.js +321 -0
- package/src/lib/merkle/filemeta.js +43 -0
- package/src/lib/merkle/git-ignore.js +24 -0
- package/src/lib/merkle/hash.js +1 -0
- package/src/lib/merkle/index.js +22 -0
- package/src/lib/merkle/persistence.js +72 -0
- package/src/lib/merkle/semantic.js +359 -0
- package/src/lib/merkle/snapshot.js +9 -3
- package/src/lib/merkle/tree.js +11 -6
- package/src/lib/project-config.js +227 -0
- package/src/lib/project-rules.js +68 -0
- package/src/lib/provider-credentials.js +83 -0
- package/src/lib/save-sdk-token.js +1 -1
- 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 +22 -15
- package/src/lib/tools.js +11 -5
- package/src/models/catalog.js +135 -0
- package/src/models/index.js +7 -0
- package/src/providers/index.js +5 -0
- package/src/providers/openai-responses.js +275 -0
- package/src/security/index.js +1 -0
- package/src/security/inventory.js +4 -1
- package/src/security/process.js +35 -9
- package/src/security/render.js +27 -5
- package/src/security/scan.js +24 -9
- package/src/security/trust-boundary.js +24 -0
- package/src/skills/index.js +64 -0
- package/src/telemetry/contracts.js +203 -0
- package/src/telemetry/events.js +48 -0
- package/src/telemetry/index.js +8 -0
- package/src/tools/hydrate.js +35 -0
- package/src/tools/index.js +2 -0
- package/src/tools/search.js +70 -0
- package/src/ui/ansi.js +1 -1
- package/src/ui/boot.js +54 -0
- package/src/ui/merkle/render.js +1 -0
- package/src/ui/runtime-activity.js +192 -0
- package/src/ui/theme.js +19 -18
- package/test/account-session.test.mjs +36 -0
- package/test/app-building-skills.test.mjs +61 -0
- package/test/apps-scaffold-contract.test.mjs +56 -0
- package/test/capabilities.test.mjs +53 -4
- package/test/cli-preferences.test.mjs +46 -0
- package/test/cloudflare-connector.test.mjs +96 -0
- package/test/cloudflare-runtime.test.mjs +75 -0
- package/test/context.test.mjs +144 -0
- package/test/deploy-health-scan.test.mjs +67 -0
- package/test/error-diagnostics.test.mjs +59 -0
- package/test/eval-context.test.mjs +37 -0
- package/test/execution-approvals.test.mjs +27 -0
- package/test/indexing-provider.test.mjs +29 -0
- package/test/jr-dev-skill.test.mjs +26 -0
- package/test/knowledge-context-pack.test.mjs +19 -0
- package/test/knowledge.test.mjs +1 -1
- package/test/local-sessions.test.mjs +42 -0
- package/test/local-studio-deploy.test.mjs +83 -0
- package/test/merkle-persistence.test.mjs +91 -0
- package/test/merkle.test.mjs +77 -3
- package/test/model-catalog.test.mjs +43 -0
- package/test/models.test.mjs +51 -0
- package/test/npm10-lock.test.mjs +29 -0
- package/test/ollama.test.mjs +94 -0
- package/test/openai-responses.test.mjs +95 -0
- package/test/project-config.test.mjs +81 -0
- package/test/project-rules.test.mjs +44 -0
- package/test/provider-credentials.test.mjs +52 -0
- package/test/rehydrate.test.mjs +25 -0
- package/test/release-hygiene.test.mjs +34 -0
- package/test/repository-snapshot-view.test.mjs +112 -0
- package/test/responses-runner.test.mjs +148 -0
- package/test/runtime-activity.test.mjs +98 -0
- package/test/sdk-worker-contract.test.mjs +68 -0
- package/test/security.test.mjs +46 -0
- package/test/shell.test.mjs +55 -21
- package/test/skills.test.mjs +22 -0
- package/test/smoke.mjs +5 -2
- package/test/telemetry.test.mjs +79 -0
- package/test/theme-portability.test.mjs +14 -0
- package/test/tools-search.test.mjs +40 -0
- package/test/whoami-resume.test.mjs +56 -0
- package/examples/agentsam-tui-ansi.mjs +0 -149
- package/src/commands/tui.js +0 -120
- package/src/ui/splash-xterm.js +0 -290
- package/src/ui/splash.js +0 -426
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { assertProductionDeployAllowed, inspectDeployGit } from './git-guard.js';
|
|
6
|
+
import { assertNoDeploySecrets } from './secret-scan.js';
|
|
7
|
+
import { parseWranglerVersionId, probeDeployHealth, resolveHealthOrigin } from './health.js';
|
|
8
|
+
|
|
9
|
+
export const LOCAL_STUDIO_REL = 'apps/local-studio';
|
|
10
|
+
export const WRANGLER_CONFIG_REL = 'backend/wrangler.jsonc';
|
|
11
|
+
export const DEPLOY_INPUT_GLOBS = Object.freeze([
|
|
12
|
+
'apps/local-studio',
|
|
13
|
+
'packages/agentsam-contracts',
|
|
14
|
+
'packages/agentsam-workbench',
|
|
15
|
+
'packages/connectors/cloudflare',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function findRepoRoot(start = process.cwd()) {
|
|
19
|
+
let dir = path.resolve(start);
|
|
20
|
+
for (;;) {
|
|
21
|
+
if (fs.existsSync(path.join(dir, 'apps/local-studio/backend/wrangler.jsonc'))) return dir;
|
|
22
|
+
if (fs.existsSync(path.join(dir, 'backend/wrangler.jsonc')) && path.basename(dir) === 'local-studio') {
|
|
23
|
+
return path.dirname(path.dirname(dir));
|
|
24
|
+
}
|
|
25
|
+
const parent = path.dirname(dir);
|
|
26
|
+
if (parent === dir) return start;
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveLocalStudioDeployable(cwd = process.cwd()) {
|
|
32
|
+
const repoRoot = findRepoRoot(cwd);
|
|
33
|
+
const appRoot = path.join(repoRoot, LOCAL_STUDIO_REL);
|
|
34
|
+
const wranglerConfig = path.join(appRoot, WRANGLER_CONFIG_REL);
|
|
35
|
+
if (!fs.existsSync(wranglerConfig)) {
|
|
36
|
+
throw new Error(`wrangler config missing: ${wranglerConfig}`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
repoRoot,
|
|
40
|
+
appRoot,
|
|
41
|
+
wranglerConfig,
|
|
42
|
+
wranglerArgs: ['deploy', '-c', WRANGLER_CONFIG_REL],
|
|
43
|
+
provider: 'cloudflare',
|
|
44
|
+
app: 'local-studio',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadOptionalCloudflareEnv(appRoot) {
|
|
49
|
+
const file = path.join(appRoot, '.env.cloudflare');
|
|
50
|
+
if (!fs.existsSync(file)) return { loaded: false, vars: {} };
|
|
51
|
+
const vars = {};
|
|
52
|
+
for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
53
|
+
const trimmed = line.trim();
|
|
54
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
55
|
+
const eq = trimmed.indexOf('=');
|
|
56
|
+
if (eq < 1) continue;
|
|
57
|
+
const key = trimmed.slice(0, eq).trim();
|
|
58
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
59
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
60
|
+
value = value.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
vars[key] = value;
|
|
63
|
+
}
|
|
64
|
+
return { loaded: true, vars };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hashFile(file) {
|
|
68
|
+
const h = crypto.createHash('sha256');
|
|
69
|
+
h.update(fs.readFileSync(file));
|
|
70
|
+
return h.digest('hex');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function walkFiles(dir, acc = []) {
|
|
74
|
+
if (!fs.existsSync(dir)) return acc;
|
|
75
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
76
|
+
if (ent.name === 'node_modules' || ent.name === '.output' || ent.name === '.wrangler' || ent.name === '.git') continue;
|
|
77
|
+
const full = path.join(dir, ent.name);
|
|
78
|
+
if (ent.isDirectory()) walkFiles(full, acc);
|
|
79
|
+
else acc.push(full);
|
|
80
|
+
}
|
|
81
|
+
return acc;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function computeDeployFingerprint(repoRoot) {
|
|
85
|
+
const h = crypto.createHash('sha256');
|
|
86
|
+
const files = [];
|
|
87
|
+
for (const rel of DEPLOY_INPUT_GLOBS) {
|
|
88
|
+
walkFiles(path.join(repoRoot, rel), files);
|
|
89
|
+
}
|
|
90
|
+
files.sort();
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const rel = path.relative(repoRoot, file).split(path.sep).join('/');
|
|
93
|
+
h.update(rel);
|
|
94
|
+
h.update('\0');
|
|
95
|
+
h.update(hashFile(file));
|
|
96
|
+
h.update('\n');
|
|
97
|
+
}
|
|
98
|
+
return h.digest('hex');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function readBaselineFingerprint(appRoot) {
|
|
102
|
+
const receipt = path.join(appRoot, '.agentsam/deploy-merkle/latest.receipt.json');
|
|
103
|
+
if (!fs.existsSync(receipt)) return null;
|
|
104
|
+
try {
|
|
105
|
+
const json = JSON.parse(fs.readFileSync(receipt, 'utf8'));
|
|
106
|
+
return json.deployProjectionHash || json.fingerprint || null;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function writeDeployReceipt(appRoot, receipt) {
|
|
113
|
+
const dir = path.join(appRoot, '.agentsam/deploy-merkle');
|
|
114
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
115
|
+
const file = path.join(dir, receipt.promoted ? 'latest.receipt.json' : 'pending.receipt.json');
|
|
116
|
+
fs.writeFileSync(file, JSON.stringify(receipt, null, 2) + '\n');
|
|
117
|
+
return file;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function wranglerDeployCommand(target, { dryRun = false } = {}) {
|
|
121
|
+
const args = ['wrangler', ...target.wranglerArgs];
|
|
122
|
+
if (dryRun) args.push('--dry-run');
|
|
123
|
+
return { cwd: target.appRoot, args, bin: 'npx' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isLocalStudioCheckout(cwd = process.cwd()) {
|
|
127
|
+
try {
|
|
128
|
+
resolveLocalStudioDeployable(cwd);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function runLocalStudioDeploy({
|
|
136
|
+
cwd = process.cwd(),
|
|
137
|
+
dryRun = false,
|
|
138
|
+
planOnly = false,
|
|
139
|
+
skipBuild = false,
|
|
140
|
+
execute = true,
|
|
141
|
+
skipSecretScan = false,
|
|
142
|
+
probeHealth = null,
|
|
143
|
+
} = {}) {
|
|
144
|
+
const target = resolveLocalStudioDeployable(cwd);
|
|
145
|
+
if (path.resolve(cwd) === target.repoRoot && !target.wranglerConfig.endsWith(path.join('backend', 'wrangler.jsonc')) && !target.wranglerConfig.endsWith('backend/wrangler.jsonc')) {
|
|
146
|
+
throw new Error('refusing generic repo-root wrangler deploy');
|
|
147
|
+
}
|
|
148
|
+
const envFile = loadOptionalCloudflareEnv(target.appRoot);
|
|
149
|
+
const fingerprint = computeDeployFingerprint(target.repoRoot);
|
|
150
|
+
const baseline = readBaselineFingerprint(target.appRoot);
|
|
151
|
+
const gitInfo = inspectDeployGit(target.repoRoot);
|
|
152
|
+
const wranglerText = fs.readFileSync(target.wranglerConfig, 'utf8');
|
|
153
|
+
const healthOrigin = resolveHealthOrigin({ env: { ...process.env, ...envFile.vars }, wranglerConfigText: wranglerText });
|
|
154
|
+
const plan = {
|
|
155
|
+
provider: 'cloudflare',
|
|
156
|
+
app: 'local-studio',
|
|
157
|
+
wranglerConfig: path.relative(target.repoRoot, target.wranglerConfig).split(path.sep).join('/'),
|
|
158
|
+
wranglerArgs: target.wranglerArgs,
|
|
159
|
+
cwd: path.relative(target.repoRoot, target.appRoot).split(path.sep).join('/') || '.',
|
|
160
|
+
envLoaded: envFile.loaded,
|
|
161
|
+
fingerprint,
|
|
162
|
+
baseline,
|
|
163
|
+
skip: Boolean(baseline && baseline === fingerprint && !dryRun && !planOnly),
|
|
164
|
+
genericRootDeploy: false,
|
|
165
|
+
productionGitRequired: Boolean(execute && !dryRun && !planOnly),
|
|
166
|
+
healthOrigin,
|
|
167
|
+
git: {
|
|
168
|
+
branch: gitInfo.branch,
|
|
169
|
+
head: gitInfo.head,
|
|
170
|
+
originMain: gitInfo.originMain,
|
|
171
|
+
equal: Boolean(gitInfo.head && gitInfo.head === gitInfo.originMain),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
if (!skipSecretScan) {
|
|
175
|
+
plan.secretScan = assertNoDeploySecrets([
|
|
176
|
+
target.wranglerConfig,
|
|
177
|
+
path.join(target.appRoot, '.env.cloudflare.example'),
|
|
178
|
+
path.join(target.appRoot, 'backend/worker/index.js'),
|
|
179
|
+
path.join(target.repoRoot, 'packages/connectors/cloudflare/src/index.js'),
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
if (plan.skip) {
|
|
183
|
+
const receipt = {
|
|
184
|
+
ok: true,
|
|
185
|
+
skipped: true,
|
|
186
|
+
reason: 'unchanged_deploy_fingerprint',
|
|
187
|
+
deployProjectionHash: fingerprint,
|
|
188
|
+
promoted: false,
|
|
189
|
+
git: plan.git,
|
|
190
|
+
wranglerConfig: plan.wranglerConfig,
|
|
191
|
+
hostname: healthOrigin.replace(/^https?:\/\//, ''),
|
|
192
|
+
};
|
|
193
|
+
writeDeployReceipt(target.appRoot, receipt);
|
|
194
|
+
return { target, plan, receipt };
|
|
195
|
+
}
|
|
196
|
+
if (!execute || planOnly) {
|
|
197
|
+
return {
|
|
198
|
+
target,
|
|
199
|
+
plan,
|
|
200
|
+
receipt: {
|
|
201
|
+
ok: true,
|
|
202
|
+
skipped: false,
|
|
203
|
+
plan: true,
|
|
204
|
+
dryRun,
|
|
205
|
+
deployProjectionHash: fingerprint,
|
|
206
|
+
promoted: false,
|
|
207
|
+
git: plan.git,
|
|
208
|
+
wranglerConfig: plan.wranglerConfig,
|
|
209
|
+
hostname: healthOrigin.replace(/^https?:\/\//, ''),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!dryRun) {
|
|
215
|
+
plan.git = assertProductionDeployAllowed(target.repoRoot);
|
|
216
|
+
plan.git.equal = plan.git.head === plan.git.originMain;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const env = { ...process.env, ...envFile.vars };
|
|
220
|
+
const lock = spawnSync('npm', ['run', 'verify:npm10-lock'], { cwd: target.appRoot, env, encoding: 'utf8' });
|
|
221
|
+
if (lock.status !== 0) throw new Error(lock.stderr || lock.stdout || 'verify:npm10-lock failed');
|
|
222
|
+
if (!skipBuild) {
|
|
223
|
+
const build = spawnSync('npm', ['run', 'build'], { cwd: target.appRoot, env, encoding: 'utf8' });
|
|
224
|
+
if (build.status !== 0) throw new Error(build.stderr || build.stdout || 'build failed');
|
|
225
|
+
const verify = spawnSync('npm', ['run', 'cf:verify-output'], { cwd: target.appRoot, env, encoding: 'utf8' });
|
|
226
|
+
if (verify.status !== 0) throw new Error(verify.stderr || verify.stdout || 'cf:verify-output failed');
|
|
227
|
+
}
|
|
228
|
+
const cmd = wranglerDeployCommand(target, { dryRun });
|
|
229
|
+
const deployed = spawnSync(cmd.bin, cmd.args, { cwd: cmd.cwd, env, encoding: 'utf8' });
|
|
230
|
+
const wranglerOut = `${deployed.stdout || ''}\n${deployed.stderr || ''}`;
|
|
231
|
+
const workerVersion = parseWranglerVersionId(wranglerOut);
|
|
232
|
+
if (deployed.status !== 0) {
|
|
233
|
+
const receipt = {
|
|
234
|
+
ok: false,
|
|
235
|
+
skipped: false,
|
|
236
|
+
deployProjectionHash: fingerprint,
|
|
237
|
+
promoted: false,
|
|
238
|
+
git: plan.git,
|
|
239
|
+
workerVersion,
|
|
240
|
+
error: wranglerOut,
|
|
241
|
+
};
|
|
242
|
+
writeDeployReceipt(target.appRoot, receipt);
|
|
243
|
+
throw new Error(receipt.error || 'wrangler deploy failed');
|
|
244
|
+
}
|
|
245
|
+
let health = null;
|
|
246
|
+
if (!dryRun) {
|
|
247
|
+
const probe = probeHealth || probeDeployHealth;
|
|
248
|
+
health = await probe(healthOrigin);
|
|
249
|
+
if (!health.ok) {
|
|
250
|
+
const receipt = {
|
|
251
|
+
ok: false,
|
|
252
|
+
skipped: false,
|
|
253
|
+
deployProjectionHash: fingerprint,
|
|
254
|
+
promoted: false,
|
|
255
|
+
git: plan.git,
|
|
256
|
+
workerVersion,
|
|
257
|
+
hostname: healthOrigin.replace(/^https?:\/\//, ''),
|
|
258
|
+
health,
|
|
259
|
+
error: 'postdeploy_health_failed',
|
|
260
|
+
};
|
|
261
|
+
writeDeployReceipt(target.appRoot, receipt);
|
|
262
|
+
throw new Error('postdeploy_health_failed');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const receipt = {
|
|
266
|
+
ok: true,
|
|
267
|
+
skipped: false,
|
|
268
|
+
dryRun,
|
|
269
|
+
deployProjectionHash: fingerprint,
|
|
270
|
+
promoted: !dryRun,
|
|
271
|
+
provider: 'cloudflare',
|
|
272
|
+
app: 'local-studio',
|
|
273
|
+
wranglerConfig: plan.wranglerConfig,
|
|
274
|
+
git: plan.git,
|
|
275
|
+
worker: 'agentsam-sdk',
|
|
276
|
+
workerVersion,
|
|
277
|
+
hostname: healthOrigin.replace(/^https?:\/\//, ''),
|
|
278
|
+
health,
|
|
279
|
+
originMainEqual: Boolean(plan.git?.head && plan.git.head === plan.git.originMain),
|
|
280
|
+
};
|
|
281
|
+
writeDeployReceipt(target.appRoot, receipt);
|
|
282
|
+
return { target, plan, receipt };
|
|
283
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Scan deployable files for live secrets. Fixtures and .example placeholders are allowed. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
const FIXTURES = new Set([
|
|
5
|
+
'sillynotreal',
|
|
6
|
+
'sillynotreal-secret',
|
|
7
|
+
'CHANGEME',
|
|
8
|
+
'replace-me',
|
|
9
|
+
'your-token-here',
|
|
10
|
+
'***',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const LIVE_PATTERNS = [
|
|
14
|
+
{ re: /\bsk-[A-Za-z0-9]{20,}\b/g, label: 'openai-like' },
|
|
15
|
+
{ re: /\bxai-[A-Za-z0-9]{20,}\b/g, label: 'xai-like' },
|
|
16
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b/g, label: 'aws-like' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const ASSIGN_RE = /((?:CLOUDFLARE_OAUTH_CLIENT_SECRET|CLOUDFLARE_API_TOKEN|IAM_CLIENT_SECRET|VAULT_MASTER_KEY)\s*=\s*)([^\s#]+)/g;
|
|
20
|
+
|
|
21
|
+
function isExample(filename = '') {
|
|
22
|
+
return filename.endsWith('.example') || filename.includes('.env.cloudflare.example');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function scanTextForSecrets(text, { filename = '' } = {}) {
|
|
26
|
+
const findings = [];
|
|
27
|
+
const src = String(text || '');
|
|
28
|
+
for (const { re, label } of LIVE_PATTERNS) {
|
|
29
|
+
re.lastIndex = 0;
|
|
30
|
+
let m;
|
|
31
|
+
while ((m = re.exec(src))) {
|
|
32
|
+
if (FIXTURES.has(m[0])) continue;
|
|
33
|
+
findings.push({ filename, label, preview: `${m[0].slice(0, 5)}…` });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (isExample(filename)) return findings;
|
|
37
|
+
ASSIGN_RE.lastIndex = 0;
|
|
38
|
+
let m;
|
|
39
|
+
while ((m = ASSIGN_RE.exec(src))) {
|
|
40
|
+
const val = m[2].replace(/^[\'"]|[\'"]$/g, '');
|
|
41
|
+
if (!val || FIXTURES.has(val) || val.includes('your-') || val.length < 16) continue;
|
|
42
|
+
findings.push({ filename, label: 'assigned-secret', preview: `${m[1].trim()}[redacted]` });
|
|
43
|
+
}
|
|
44
|
+
return findings;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function scanFilesForSecrets(files = []) {
|
|
48
|
+
const findings = [];
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
if (!file || !fs.existsSync(file) || !fs.statSync(file).isFile()) continue;
|
|
51
|
+
findings.push(...scanTextForSecrets(fs.readFileSync(file, 'utf8'), { filename: file }));
|
|
52
|
+
}
|
|
53
|
+
return findings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function assertNoDeploySecrets(files = []) {
|
|
57
|
+
const findings = scanFilesForSecrets(files);
|
|
58
|
+
if (findings.length) {
|
|
59
|
+
const err = new Error('secret_scan_failed: ' + findings.map((f) => f.label).join(', '));
|
|
60
|
+
err.code = 'secret_scan_failed';
|
|
61
|
+
err.findings = findings;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, findings: [] };
|
|
65
|
+
}
|
|
@@ -146,7 +146,7 @@ export async function captureDeployReceipt({
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
const tree = await buildMerkleTree(rootPath, { policy });
|
|
149
|
+
const tree = await buildMerkleTree(rootPath, { policy, semantic: true });
|
|
150
150
|
await atomicJson(state.pendingSnapshot, tree);
|
|
151
151
|
|
|
152
152
|
let diff = null;
|
|
@@ -163,7 +163,7 @@ export async function captureDeployReceipt({
|
|
|
163
163
|
const receipt = {
|
|
164
164
|
format: 'agentsam-deploy-merkle',
|
|
165
165
|
version: 1,
|
|
166
|
-
engine: 'agentsam-merkle-v1',
|
|
166
|
+
engine: tree.semantic ? 'agentsam-merkle-v1+agentsam-filemeta-v1' : 'agentsam-merkle-v1',
|
|
167
167
|
status: 'captured',
|
|
168
168
|
project: project || path.basename(rootPath),
|
|
169
169
|
repository: gitInfo.repository || null,
|
|
@@ -171,6 +171,9 @@ export async function captureDeployReceipt({
|
|
|
171
171
|
git_branch: gitInfo.git_branch || null,
|
|
172
172
|
captured_at: new Date().toISOString(),
|
|
173
173
|
root_hash: tree.rootHash,
|
|
174
|
+
policy_hash: tree.policyHash || null,
|
|
175
|
+
metadata_root: tree.semantic?.rootHash || null,
|
|
176
|
+
classifier: tree.semantic?.classifier || null,
|
|
174
177
|
previous_root_hash: baseline?.rootHash || null,
|
|
175
178
|
has_baseline: Boolean(baseline),
|
|
176
179
|
baseline_source: resolvedBaselineSource,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { execFile } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
import { coreBaseUrl } from './core-client.js';
|
|
8
|
+
import { resolveAccountSdkKey } from './account-session.js';
|
|
8
9
|
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
|
|
@@ -220,22 +221,22 @@ async function detectIam(explicitToken = '') {
|
|
|
220
221
|
});
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
const sdkToken =
|
|
224
|
+
const sdkToken = resolveAccountSdkKey({ env: process.env, explicit: explicitToken }).value;
|
|
224
225
|
if (sdkToken.trim()) {
|
|
225
226
|
const probe = await probeSdkBearer(sdkToken);
|
|
226
227
|
if (probe.valid) {
|
|
227
228
|
return {
|
|
228
|
-
source: 'sdk-
|
|
229
|
+
source: 'sdk-key',
|
|
229
230
|
ready: true,
|
|
230
|
-
detail: `
|
|
231
|
+
detail: `AGENTSAM_SDK_KEY · user ${probe.user_id || '?'}`,
|
|
231
232
|
probe,
|
|
232
233
|
aux,
|
|
233
234
|
};
|
|
234
235
|
}
|
|
235
236
|
return {
|
|
236
|
-
source: 'sdk-
|
|
237
|
+
source: 'sdk-key',
|
|
237
238
|
ready: false,
|
|
238
|
-
detail: `
|
|
239
|
+
detail: `AGENTSAM_SDK_KEY invalid (${probe.error}) → will open browser`,
|
|
239
240
|
probe,
|
|
240
241
|
aux,
|
|
241
242
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const SCHEMA = 'agentsam-execution-approvals-v1';
|
|
6
|
+
|
|
7
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
8
|
+
function homeDirectory(options = {}) { return path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir()); }
|
|
9
|
+
function approvalsPath(options = {}) { return path.join(homeDirectory(options), '.agentsam', 'execution-approvals.json'); }
|
|
10
|
+
|
|
11
|
+
function readStore(options = {}) {
|
|
12
|
+
const filename = approvalsPath(options);
|
|
13
|
+
if (!fs.existsSync(filename)) return { schema_version: SCHEMA, approvals: [] };
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
16
|
+
if (parsed?.schema_version !== SCHEMA || !Array.isArray(parsed.approvals)) return { schema_version: SCHEMA, approvals: [] };
|
|
17
|
+
return parsed;
|
|
18
|
+
} catch { return { schema_version: SCHEMA, approvals: [] }; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeStore(store, options = {}) {
|
|
22
|
+
const filename = approvalsPath(options);
|
|
23
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true, mode: 0o700 });
|
|
24
|
+
const temp = `${filename}.${process.pid}.tmp`;
|
|
25
|
+
fs.writeFileSync(temp, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
|
|
26
|
+
if (process.platform !== 'win32') {
|
|
27
|
+
try { fs.chmodSync(temp, 0o600); } catch { /* best effort */ }
|
|
28
|
+
}
|
|
29
|
+
fs.renameSync(temp, filename);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toolApprovalKey(capabilityId, input = {}) {
|
|
33
|
+
const id = clean(capabilityId);
|
|
34
|
+
const command = clean(input?.command);
|
|
35
|
+
if (id === 'cloudflare.wrangler.native' && /^[A-Za-z0-9._-]+$/.test(command)) return `${id}:${command}`;
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isExecutionApproved({ cwd, key }, options = {}) {
|
|
40
|
+
const root = path.resolve(clean(cwd) || process.cwd());
|
|
41
|
+
const target = clean(key);
|
|
42
|
+
if (!target) return false;
|
|
43
|
+
return readStore(options).approvals.some((row) => row?.cwd === root && row?.key === target);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function grantExecutionApproval({ cwd, key, label = '' }, options = {}) {
|
|
47
|
+
const root = path.resolve(clean(cwd) || process.cwd());
|
|
48
|
+
const target = clean(key);
|
|
49
|
+
if (!target) throw new Error('execution_approval_key_required');
|
|
50
|
+
const store = readStore(options);
|
|
51
|
+
const approvals = store.approvals.filter((row) => !(row?.cwd === root && row?.key === target));
|
|
52
|
+
approvals.push({ cwd: root, key: target, label: clean(label) || target, created_at: new Date().toISOString() });
|
|
53
|
+
writeStore({ schema_version: SCHEMA, approvals }, options);
|
|
54
|
+
return { cwd: root, key: target, persisted: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function listExecutionApprovals(options = {}) {
|
|
58
|
+
return readStore(options).approvals.map((row) => ({ ...row }));
|
|
59
|
+
}
|
|
@@ -75,7 +75,7 @@ migrations_dir = "migrations"
|
|
|
75
75
|
files['.env.example'] = `# Minted at install/build — secrets via wrangler secret put (never commit)
|
|
76
76
|
# IAM_CLIENT_ID= # plaintext var (public by OAuth design)
|
|
77
77
|
# IAM_CLIENT_SECRET= # wrangler secret put only
|
|
78
|
-
#
|
|
78
|
+
# IAM_ORIGIN=https://inneranimalmedia.com
|
|
79
79
|
|
|
80
80
|
# Developer BYOK — takes /api/oauth/{google|github}/start when set
|
|
81
81
|
# GOOGLE_CLIENT_ID=
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import pkg from '../../package.json' with { type: 'json' };
|
|
7
|
+
import { createProjectManifest } from './project-config.js';
|
|
8
|
+
import { defaultProjectRules } from './project-rules.js';
|
|
7
9
|
|
|
8
10
|
const LANE_KEYS = {
|
|
9
11
|
'1': 'fullstack',
|
|
@@ -139,37 +141,24 @@ export function buildLocalScaffoldFiles({
|
|
|
139
141
|
const migration = migrationSql(laneKey);
|
|
140
142
|
|
|
141
143
|
return [
|
|
142
|
-
{
|
|
143
|
-
path: 'agentsam.config.js',
|
|
144
|
-
content: `export default {
|
|
145
|
-
project: '${projectName}',
|
|
146
|
-
lane: '${laneKey}',
|
|
147
|
-
agent: '${agent}',
|
|
148
|
-
runTarget: '${runTarget}',
|
|
149
|
-
api: { baseUrl: '/api/agentsam' },
|
|
150
|
-
};
|
|
151
|
-
`,
|
|
152
|
-
},
|
|
153
144
|
{
|
|
154
145
|
path: '.agentsam/config.json',
|
|
155
146
|
content: `${JSON.stringify(
|
|
156
|
-
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
agent,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
dev_port: 8787,
|
|
164
|
-
db_path: '.agentsam/data/agentsam.sqlite',
|
|
165
|
-
db_schema: 'db/schema.sql',
|
|
166
|
-
ui: 'terminal',
|
|
167
|
-
scaffold_version: sdkVersion,
|
|
168
|
-
},
|
|
147
|
+
createProjectManifest({
|
|
148
|
+
projectName,
|
|
149
|
+
preset: laneKey,
|
|
150
|
+
profile: agent,
|
|
151
|
+
runTarget,
|
|
152
|
+
sdkVersion,
|
|
153
|
+
}),
|
|
169
154
|
null,
|
|
170
155
|
2,
|
|
171
156
|
)}\n`,
|
|
172
157
|
},
|
|
158
|
+
{
|
|
159
|
+
path: '.agentsamrules',
|
|
160
|
+
content: defaultProjectRules(projectName),
|
|
161
|
+
},
|
|
173
162
|
{
|
|
174
163
|
path: '.agentsam/start-local.md',
|
|
175
164
|
content: `# Agent Sam local project
|
|
@@ -187,9 +176,10 @@ Useful local surfaces:
|
|
|
187
176
|
|
|
188
177
|
\`\`\`bash
|
|
189
178
|
npm run db:status
|
|
190
|
-
|
|
191
|
-
npm run tui:rich -- --install # optional Rich UI in isolated .agentsam venv
|
|
179
|
+
npx agentsam # enter Agent Sam
|
|
192
180
|
npx agentsam start-local # PTY on ws://127.0.0.1:3099
|
|
181
|
+
npm run ollama:setup # optional local/free model config
|
|
182
|
+
npm run ollama:status # probe local Ollama + models
|
|
193
183
|
\`\`\`
|
|
194
184
|
|
|
195
185
|
The local SQLite database lives at \`.agentsam/data/agentsam.sqlite\` and is initialized from \`db/schema.sql\`.
|
|
@@ -209,7 +199,12 @@ npx agentsam deploy
|
|
|
209
199
|
dist/
|
|
210
200
|
.wrangler/
|
|
211
201
|
.agentsam/data/
|
|
202
|
+
.agentsam/cli.json
|
|
212
203
|
.agentsam/tui-venv/
|
|
204
|
+
.agentsam/knowledge/
|
|
205
|
+
.agentsam/cache/
|
|
206
|
+
.agentsam/merkle/
|
|
207
|
+
.agentsam/merkle.json
|
|
213
208
|
*.db
|
|
214
209
|
*.sqlite
|
|
215
210
|
*.sqlite-*
|
|
@@ -231,8 +226,8 @@ dist/
|
|
|
231
226
|
'db:init': 'agentsam db init',
|
|
232
227
|
'db:status': 'agentsam db status',
|
|
233
228
|
pty: 'agentsam start-local',
|
|
234
|
-
|
|
235
|
-
'
|
|
229
|
+
'ollama:setup': 'agentsam ollama setup',
|
|
230
|
+
'ollama:status': 'agentsam ollama status',
|
|
236
231
|
deploy: 'agentsam deploy',
|
|
237
232
|
},
|
|
238
233
|
engines: {
|
|
@@ -262,6 +257,11 @@ AGENTSAM_LANE=${laneKey}
|
|
|
262
257
|
AGENTSAM_AGENT=${agent}
|
|
263
258
|
AGENTSAM_DB=.agentsam/data/agentsam.sqlite
|
|
264
259
|
PORT=8787
|
|
260
|
+
|
|
261
|
+
# Optional free/local model runtime. Enable with: agentsam ollama setup
|
|
262
|
+
# OLLAMA_BASE_URL=http://127.0.0.1:11434
|
|
263
|
+
# OLLAMA_MODEL=qwen2.5-coder
|
|
264
|
+
# OLLAMA_EMBED_MODEL=mxbai-embed-large
|
|
265
265
|
`,
|
|
266
266
|
},
|
|
267
267
|
{
|
|
@@ -399,13 +399,15 @@ Local state:
|
|
|
399
399
|
## Agent Sam terminal experience
|
|
400
400
|
|
|
401
401
|
\`\`\`bash
|
|
402
|
-
|
|
403
|
-
npm run tui:rich # Python Rich dashboard if installed
|
|
404
|
-
npm run tui:rich -- --install
|
|
402
|
+
npx agentsam # enter the interactive Agent Sam experience
|
|
405
403
|
npm run pty # local PTY on ws://127.0.0.1:3099
|
|
406
404
|
npm run db:status
|
|
405
|
+
npm run ollama:setup # optional local/free model config
|
|
406
|
+
npm run ollama:status # local Ollama + model readiness
|
|
407
407
|
\`\`\`
|
|
408
408
|
|
|
409
|
+
Ollama is optional and local-only; it is not deployed as an edge model. Remote AgentSam sessions can reach it by executing the CLI through the existing user-hosted terminal tunnel.
|
|
410
|
+
|
|
409
411
|
No Worker or cloud account is required for local development.
|
|
410
412
|
|
|
411
413
|
## Graduate intentionally
|
|
@@ -443,9 +445,9 @@ export function buildLocalScaffoldMeta(body, sdkVersion = pkg.version) {
|
|
|
443
445
|
'npm run smoke',
|
|
444
446
|
'npm run status',
|
|
445
447
|
'npm run dev',
|
|
446
|
-
'
|
|
448
|
+
'npx agentsam',
|
|
447
449
|
'npm run db:status',
|
|
448
|
-
'Optional: npm run
|
|
450
|
+
'Optional free/local models: npm run ollama:setup',
|
|
449
451
|
'Optional: npm run pty',
|
|
450
452
|
'When ready for cloud: npm run deploy',
|
|
451
453
|
],
|