@inneranimalmedia/agentsam-sdk 2.4.1 → 2.5.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/README.md +7 -6
- 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 +57 -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 +26 -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 +17 -4
- package/packages/identity/package.json +11 -2
- package/packages/identity/src/contracts/auth-config.js +98 -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 +57 -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/capabilities/manifest.json +32 -3
- package/protocol/capabilities/repository-snapshot.schema.json +1 -0
- package/protocol/context/context-budget.schema.json +41 -0
- package/protocol/context/context-item.schema.json +20 -0
- package/protocol/context/resolved-context-pack.schema.json +42 -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/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-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 +53 -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 +108 -56
- package/src/commands/context.js +14 -2
- package/src/commands/db.js +4 -7
- package/src/commands/deploy.js +17 -31
- package/src/commands/interactive.js +21 -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 +107 -0
- package/src/commands/ollama.js +259 -0
- package/src/commands/preferences.js +102 -0
- package/src/commands/product.js +86 -16
- package/src/commands/security.js +3 -3
- package/src/commands/shell.js +71 -27
- package/src/commands/skills.js +66 -0
- package/src/commands/start-local.js +1 -1
- package/src/commands/tunnel.js +5 -4
- package/src/context/budget.js +58 -0
- package/src/context/compact.js +28 -0
- package/src/context/index.js +5 -0
- package/src/context/resolve.js +84 -0
- package/src/context/result-policy.js +66 -0
- package/src/index.js +16 -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/bridge-client.js +7 -5
- package/src/lib/cli-preferences.js +74 -0
- package/src/lib/core-client.js +8 -8
- package/src/lib/deploy-receipt/index.js +5 -2
- package/src/lib/detect-context.js +6 -5
- package/src/lib/identity-scaffold.js +1 -1
- package/src/lib/local-scaffold.js +35 -33
- 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/save-sdk-token.js +1 -1
- package/src/lib/slash-commands.js +2 -1
- package/src/lib/tools.js +11 -5
- package/src/security/index.js +1 -0
- package/src/security/inventory.js +4 -1
- 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/tools/index.js +1 -0
- package/src/tools/search.js +70 -0
- package/src/ui/ansi.js +1 -1
- package/src/ui/boot.js +56 -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/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 +25 -0
- package/test/context.test.mjs +95 -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/merkle-persistence.test.mjs +91 -0
- package/test/merkle.test.mjs +77 -3
- package/test/models.test.mjs +37 -0
- package/test/ollama.test.mjs +94 -0
- package/test/project-config.test.mjs +81 -0
- package/test/project-rules.test.mjs +44 -0
- package/test/release-hygiene.test.mjs +34 -0
- package/test/repository-snapshot-view.test.mjs +112 -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 +8 -1
- package/test/skills.test.mjs +22 -0
- package/test/smoke.mjs +2 -2
- package/test/theme-portability.test.mjs +14 -0
- package/test/tools-search.test.mjs +27 -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
package/src/ui/boot.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
|
|
4
|
+
const FRAMES = ['◔', '◑', '◕', '●'];
|
|
5
|
+
const CLEAR_LINE = '\x1b[2K';
|
|
6
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
7
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
8
|
+
|
|
9
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
|
|
11
|
+
function compactHome(value) {
|
|
12
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
13
|
+
if (home && value.startsWith(home)) return `~${value.slice(home.length)}`;
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderBootSummary({ identity, preferences }) {
|
|
18
|
+
const branch = identity.branch ? ` · ${pc.cyan(identity.branch)}` : '';
|
|
19
|
+
const model = preferences.modelPreference && preferences.modelPreference !== 'auto'
|
|
20
|
+
? preferences.modelPreference
|
|
21
|
+
: 'auto';
|
|
22
|
+
const runtime = preferences.runtime || 'local';
|
|
23
|
+
const terminal = preferences.terminal || path.basename(process.env.SHELL || '') || 'shell';
|
|
24
|
+
return [
|
|
25
|
+
'',
|
|
26
|
+
` ${pc.bold('Agent Sam')} ${pc.green('●')}`,
|
|
27
|
+
` ${pc.cyan(identity.project)}${branch} · ${pc.white(model)}`,
|
|
28
|
+
` ${pc.dim(compactHome(identity.root))}`,
|
|
29
|
+
` ${pc.green('✓')} ${pc.dim(runtime)} · ${pc.dim(terminal)} · ready`,
|
|
30
|
+
'',
|
|
31
|
+
].join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function runBootScene({ identity, preferences, animate = true, write = process.stdout.write.bind(process.stdout) }) {
|
|
35
|
+
const interactive = Boolean(animate && process.stdout.isTTY);
|
|
36
|
+
if (!interactive) {
|
|
37
|
+
write(renderBootSummary({ identity, preferences }));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
write(HIDE_CURSOR);
|
|
42
|
+
try {
|
|
43
|
+
write(`\n ${pc.bold('Agent Sam')}\n ${pc.cyan(identity.project)}${identity.branch ? ` · ${pc.cyan(identity.branch)}` : ''}\n\n`);
|
|
44
|
+
const checks = ['project', 'runtime', 'model'];
|
|
45
|
+
for (const label of checks) {
|
|
46
|
+
for (let i = 0; i < FRAMES.length; i += 1) {
|
|
47
|
+
write(`\r${CLEAR_LINE} ${pc.cyan(FRAMES[i])} ${pc.dim(`checking ${label}`)}`);
|
|
48
|
+
await sleep(i === FRAMES.length - 1 ? 45 : 55);
|
|
49
|
+
}
|
|
50
|
+
write(`\r${CLEAR_LINE} ${pc.green('✓')} ${pc.dim(label)}\n`);
|
|
51
|
+
}
|
|
52
|
+
write(`\n ${pc.green('●')} ${pc.bold('ready')} ${pc.dim(compactHome(identity.root))}\n\n`);
|
|
53
|
+
} finally {
|
|
54
|
+
write(SHOW_CURSOR);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/ui/merkle/render.js
CHANGED
|
@@ -79,6 +79,7 @@ export function renderSummary(result, { inspect = false, color = false } = {}) {
|
|
|
79
79
|
const { tree, diff } = result;
|
|
80
80
|
const lines = [paint(`Agent Sam · ${safeText(result.title)}`, '1;36', color), ` path: ${safeText(tree.rootPath)}`,
|
|
81
81
|
` files: ${tree.stats.files} links: ${tree.stats.symlinks} bytes: ${tree.stats.bytes}`, ` root: ${tree.rootHash}`];
|
|
82
|
+
if (tree.semantic) lines.push(` meta: ${tree.semantic.rootHash} (${tree.semantic.classifier.format}/v${tree.semantic.classifier.version})`);
|
|
82
83
|
if (result.output) lines.push(` saved: ${safeText(result.output)}`);
|
|
83
84
|
if (diff) {
|
|
84
85
|
lines.push(` ${diff.equal ? 'MATCH' : 'CHANGED'} ` + Object.entries(diff.stats).map(([key, value]) => `${value} ${key}`).join(' · '));
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
const CLEAR_LINE = '\x1b[2K';
|
|
4
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
5
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
6
|
+
const SPINNER = ['◔', '◑', '◕', '●'];
|
|
7
|
+
const PANEL_WIDTH = 34;
|
|
8
|
+
const PANEL_INNER = PANEL_WIDTH - 2;
|
|
9
|
+
const PANEL_LINES = 5;
|
|
10
|
+
|
|
11
|
+
const THINK_FRAMES = [
|
|
12
|
+
[' · ·', ' ○', ' · ·'],
|
|
13
|
+
[' ✦ ·', ' (○)', ' · ✦'],
|
|
14
|
+
[' · ✦', ' (●)', ' ✦ ·'],
|
|
15
|
+
[' ✦', ' ( ◯ )', ' ✦'],
|
|
16
|
+
[' ✦ ✦', ' (●)', ' ✦ ✦'],
|
|
17
|
+
[' · ✦', ' (○)', ' ✦ ·'],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function elapsedLabel(ms) {
|
|
21
|
+
if (ms < 1000) return `${Math.max(0, Math.round(ms))}ms`;
|
|
22
|
+
return `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)}s`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function truncate(value, max) {
|
|
26
|
+
const text = String(value || '');
|
|
27
|
+
if (text.length <= max) return text;
|
|
28
|
+
return `${text.slice(0, Math.max(0, max - 1))}…`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function center(value, width) {
|
|
32
|
+
const text = truncate(value, width);
|
|
33
|
+
const left = Math.max(0, Math.floor((width - text.length) / 2));
|
|
34
|
+
const right = Math.max(0, width - text.length - left);
|
|
35
|
+
return `${' '.repeat(left)}${text}${' '.repeat(right)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function titledBorder(title, width, left, right) {
|
|
39
|
+
const label = ` ${title} `;
|
|
40
|
+
const fill = Math.max(0, width - left.length - right.length - label.length);
|
|
41
|
+
return `${left}${label}${'─'.repeat(fill)}${right}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function subtitledBorder(subtitle, width, left, right) {
|
|
45
|
+
const label = ` ${truncate(subtitle, width - 4)} `;
|
|
46
|
+
const fill = Math.max(0, width - left.length - right.length - label.length);
|
|
47
|
+
return `${left}${label}${'─'.repeat(fill)}${right}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function canAnimateTerminal({ stdout = process.stdout, env = process.env } = {}) {
|
|
51
|
+
if (!stdout?.isTTY) return false;
|
|
52
|
+
if (String(env.TERM || '').toLowerCase() === 'dumb') return false;
|
|
53
|
+
if (String(env.CI || '').trim()) return false;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderRuntimeActivityFrame({
|
|
58
|
+
label = 'Agent Sam',
|
|
59
|
+
phase = 'thinking',
|
|
60
|
+
tick = 0,
|
|
61
|
+
elapsedMs = 0,
|
|
62
|
+
} = {}) {
|
|
63
|
+
const frame = SPINNER[Math.abs(Number(tick) || 0) % SPINNER.length];
|
|
64
|
+
return ` ${pc.cyan(frame)} ${pc.bold(label)} ${pc.dim(`· ${phase}`)} ${pc.dim(`· ${elapsedLabel(elapsedMs)}`)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function renderRuntimeActivityPanel({
|
|
68
|
+
label = 'Agent Sam',
|
|
69
|
+
phase = 'thinking',
|
|
70
|
+
tick = 0,
|
|
71
|
+
elapsedMs = 0,
|
|
72
|
+
status = 'active',
|
|
73
|
+
locality = 'working locally',
|
|
74
|
+
} = {}) {
|
|
75
|
+
const frame = THINK_FRAMES[Math.abs(Number(tick) || 0) % THINK_FRAMES.length];
|
|
76
|
+
const border = status === 'success' ? pc.green : status === 'error' ? pc.red : pc.cyan;
|
|
77
|
+
const body = status === 'error' ? pc.red : pc.cyan;
|
|
78
|
+
const title = titledBorder(label, PANEL_WIDTH, '╭─', '╮');
|
|
79
|
+
const subtitle = status === 'active'
|
|
80
|
+
? `${phase} · ${locality}`
|
|
81
|
+
: `${phase} · ${elapsedLabel(elapsedMs)}`;
|
|
82
|
+
return [
|
|
83
|
+
border(title),
|
|
84
|
+
`${border('│')}${body(center(frame[0], PANEL_INNER))}${border('│')}`,
|
|
85
|
+
`${border('│')}${body(center(frame[1], PANEL_INNER))}${border('│')}`,
|
|
86
|
+
`${border('│')}${body(center(frame[2], PANEL_INNER))}${border('│')}`,
|
|
87
|
+
border(subtitledBorder(subtitle, PANEL_WIDTH, '╰─', '╯')),
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function renderRuntimeActivityResult({
|
|
92
|
+
label = 'Agent Sam',
|
|
93
|
+
status = 'success',
|
|
94
|
+
phase = status === 'success' ? 'done' : 'failed',
|
|
95
|
+
elapsedMs = 0,
|
|
96
|
+
} = {}) {
|
|
97
|
+
const ok = status === 'success';
|
|
98
|
+
const symbol = ok ? pc.green('✓') : pc.red('✗');
|
|
99
|
+
return ` ${symbol} ${pc.bold(label)} ${pc.dim(`· ${phase}`)} ${pc.dim(`· ${elapsedLabel(elapsedMs)}`)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Live terminal activity for product runtime operations.
|
|
104
|
+
*
|
|
105
|
+
* This is shipped product UI, not a preview renderer. It remains quiet in pipes/CI,
|
|
106
|
+
* and only uses cursor redraw when stdout is an interactive terminal.
|
|
107
|
+
*/
|
|
108
|
+
export function createRuntimeActivity(options = {}) {
|
|
109
|
+
const write = options.write || process.stdout.write.bind(process.stdout);
|
|
110
|
+
const now = options.now || (() => Date.now());
|
|
111
|
+
const setTimer = options.setInterval || globalThis.setInterval;
|
|
112
|
+
const clearTimer = options.clearInterval || globalThis.clearInterval;
|
|
113
|
+
const interactive = options.interactive ?? canAnimateTerminal(options);
|
|
114
|
+
const intervalMs = Math.max(40, Number(options.intervalMs) || 90);
|
|
115
|
+
const label = String(options.label || 'Agent Sam');
|
|
116
|
+
const locality = String(options.locality || 'working locally');
|
|
117
|
+
|
|
118
|
+
let phase = String(options.phase || 'thinking');
|
|
119
|
+
let startedAt = 0;
|
|
120
|
+
let tick = 0;
|
|
121
|
+
let timer = null;
|
|
122
|
+
let started = false;
|
|
123
|
+
let finished = false;
|
|
124
|
+
let drawn = false;
|
|
125
|
+
|
|
126
|
+
const writePanel = (status = 'active') => {
|
|
127
|
+
if (!interactive || !started) return;
|
|
128
|
+
const elapsedMs = Math.max(0, now() - startedAt);
|
|
129
|
+
if (drawn) write(`\x1b[${PANEL_LINES}A`);
|
|
130
|
+
const panel = renderRuntimeActivityPanel({ label, phase, tick, elapsedMs, status, locality });
|
|
131
|
+
for (const line of panel.split('\n')) write(`\r${CLEAR_LINE}${line}\n`);
|
|
132
|
+
drawn = true;
|
|
133
|
+
tick += 1;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const draw = () => {
|
|
137
|
+
if (finished) return;
|
|
138
|
+
writePanel('active');
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const start = (nextPhase = phase) => {
|
|
142
|
+
if (started || finished) return;
|
|
143
|
+
phase = String(nextPhase || phase);
|
|
144
|
+
started = true;
|
|
145
|
+
startedAt = now();
|
|
146
|
+
if (!interactive) return;
|
|
147
|
+
write(HIDE_CURSOR);
|
|
148
|
+
draw();
|
|
149
|
+
timer = setTimer(draw, intervalMs);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const update = (nextPhase) => {
|
|
153
|
+
if (nextPhase) phase = String(nextPhase);
|
|
154
|
+
if (!started) start(phase);
|
|
155
|
+
draw();
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const finish = ({ status = 'success', resultPhase } = {}) => {
|
|
159
|
+
if (finished) return;
|
|
160
|
+
if (timer) clearTimer(timer);
|
|
161
|
+
timer = null;
|
|
162
|
+
phase = resultPhase || (status === 'success' ? 'done' : 'failed');
|
|
163
|
+
if (interactive && started) {
|
|
164
|
+
writePanel(status === 'success' ? 'success' : 'error');
|
|
165
|
+
write(SHOW_CURSOR);
|
|
166
|
+
}
|
|
167
|
+
finished = true;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
get interactive() { return interactive; },
|
|
172
|
+
start,
|
|
173
|
+
update,
|
|
174
|
+
succeed(phaseName = 'done') { finish({ status: 'success', resultPhase: phaseName }); },
|
|
175
|
+
fail(phaseName = 'failed') { finish({ status: 'error', resultPhase: phaseName }); },
|
|
176
|
+
stop() { finish({ status: 'success', resultPhase: 'done' }); },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Maps portable runtime events onto the customer terminal without making the renderer
|
|
182
|
+
* the authority for execution. Hosts may feed richer telemetry into the same seam.
|
|
183
|
+
*/
|
|
184
|
+
export function phaseForRuntimeEvent(event = {}) {
|
|
185
|
+
const type = String(event.type || '');
|
|
186
|
+
if (type === 'model.started' || type === 'model.delta') return 'thinking';
|
|
187
|
+
if (type === 'tool.started') return event.tool ? `using ${event.tool}` : 'using tool';
|
|
188
|
+
if (type === 'context.compaction.started') return 'tidying context';
|
|
189
|
+
if (type === 'context.compaction.completed') return 'context ready';
|
|
190
|
+
if (type === 'run.started') return 'working';
|
|
191
|
+
return null;
|
|
192
|
+
}
|
package/src/ui/theme.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/ui/theme.js
|
|
3
|
-
* @description
|
|
4
|
-
*
|
|
5
|
-
* - xterm.js panels in XTermShell.tsx (pass theme object to Terminal constructor)
|
|
6
|
-
* - CLI ANSI output in agentsam-sdk (import ANSI constants)
|
|
7
|
-
* - Splash/HUD renders in both contexts
|
|
3
|
+
* @description Agent Sam terminal theme system — portable color and semantic
|
|
4
|
+
* status tokens for native ANSI output and browser/xterm consumers.
|
|
8
5
|
*
|
|
9
|
-
* Lane identity:
|
|
10
|
-
* LOCAL → amber (
|
|
11
|
-
*
|
|
12
|
-
* SANDBOX → purple (
|
|
6
|
+
* Lane identity is intentionally provider-neutral:
|
|
7
|
+
* LOCAL → amber (the user's current machine / real shell)
|
|
8
|
+
* REMOTE → teal (a connected remote runtime, provider chosen by the host)
|
|
9
|
+
* SANDBOX → purple (an isolated/disposable runtime, provider chosen by the host)
|
|
10
|
+
*
|
|
11
|
+
* Infrastructure names, domains, machine names, and account-specific routing
|
|
12
|
+
* never belong in this SDK theme. Hosts may supply their own runtime labels.
|
|
13
13
|
*
|
|
14
14
|
* No dependencies. ESM + CJS compatible (conditional exports in package.json).
|
|
15
15
|
*/
|
|
@@ -31,7 +31,7 @@ export const PALETTE = {
|
|
|
31
31
|
ghost: '#aaaaaa',
|
|
32
32
|
white: '#e8e8e8',
|
|
33
33
|
|
|
34
|
-
// LOCAL lane — amber/gold (
|
|
34
|
+
// LOCAL lane — amber/gold (current machine / real shell)
|
|
35
35
|
amber900: '#0d0900',
|
|
36
36
|
amber800: '#1a1200',
|
|
37
37
|
amber700: '#2d1f00',
|
|
@@ -43,7 +43,7 @@ export const PALETTE = {
|
|
|
43
43
|
amber100: '#fde68a',
|
|
44
44
|
amber050: '#fef3c7',
|
|
45
45
|
|
|
46
|
-
//
|
|
46
|
+
// REMOTE lane — teal/cyan (provider-neutral connected runtime)
|
|
47
47
|
teal900: '#000d0b',
|
|
48
48
|
teal800: '#001a17',
|
|
49
49
|
teal700: '#002e28',
|
|
@@ -55,7 +55,7 @@ export const PALETTE = {
|
|
|
55
55
|
teal100: '#99f6e4',
|
|
56
56
|
teal050: '#ccfbf1',
|
|
57
57
|
|
|
58
|
-
// SANDBOX lane — purple (
|
|
58
|
+
// SANDBOX lane — purple (provider-neutral isolated runtime)
|
|
59
59
|
purple900: '#06020d',
|
|
60
60
|
purple800: '#0d0520',
|
|
61
61
|
purple700: '#1e0a3d',
|
|
@@ -130,26 +130,26 @@ const makeLane = ({ bg, bgSubtle, primary, secondary, dim, accent, name, label,
|
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
133
|
-
//
|
|
133
|
+
// PORTABLE EXECUTION LANES
|
|
134
134
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
135
135
|
|
|
136
136
|
export const LOCAL = makeLane({
|
|
137
137
|
name: 'local', label: 'Local', icon: '◈',
|
|
138
|
-
tunnelHint: '
|
|
138
|
+
tunnelHint: 'current machine · real shell',
|
|
139
139
|
bg: '#0d0900', bgSubtle: '#1a1200',
|
|
140
140
|
primary: '#f59e0b', secondary: '#fbbf24', dim: '#78450a', accent: '#22d3c8',
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
export const CLOUD = makeLane({
|
|
144
|
-
name: '
|
|
145
|
-
tunnelHint: '
|
|
144
|
+
name: 'remote', label: 'Remote', icon: '●',
|
|
145
|
+
tunnelHint: 'connected remote runtime',
|
|
146
146
|
bg: '#000d0b', bgSubtle: '#001a17',
|
|
147
147
|
primary: '#22d3c8', secondary: '#5eead4', dim: '#065f52', accent: '#f59e0b',
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
export const SANDBOX = makeLane({
|
|
151
151
|
name: 'sandbox', label: 'Sandbox', icon: '◌',
|
|
152
|
-
tunnelHint: '
|
|
152
|
+
tunnelHint: 'isolated disposable runtime',
|
|
153
153
|
bg: '#06020d', bgSubtle: '#0d0520',
|
|
154
154
|
primary: '#a78bfa', secondary: '#c4b5fd', dim: '#4c1d95', accent: '#22d3c8',
|
|
155
155
|
});
|
|
@@ -173,11 +173,12 @@ export function getLaneTheme(targetType) {
|
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
175
|
* Get lane theme by name string (for SDK CLI use).
|
|
176
|
-
* @param {'local'|'cloud'|'sandbox'|string} name
|
|
176
|
+
* @param {'local'|'remote'|'cloud'|'sandbox'|string} name
|
|
177
177
|
*/
|
|
178
178
|
export function getLaneThemeByName(name) {
|
|
179
179
|
switch (name?.toLowerCase()) {
|
|
180
180
|
case 'local': return LOCAL;
|
|
181
|
+
case 'remote':
|
|
181
182
|
case 'cloud': return CLOUD;
|
|
182
183
|
case 'sandbox': return SANDBOX;
|
|
183
184
|
default: return CLOUD;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { getSkill, listSkills, loadSkill } from '../src/skills/index.js';
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(import.meta.dirname, '..');
|
|
9
|
+
const cli = path.join(root, 'src', 'cli.js');
|
|
10
|
+
|
|
11
|
+
test('application fundamentals and progression guard are portable registered skills', () => {
|
|
12
|
+
const ids = new Set(listSkills().map((skill) => skill.id));
|
|
13
|
+
assert.ok(ids.has('agentsam-app-fundamentals'));
|
|
14
|
+
assert.ok(ids.has('agentsam-progression-guard'));
|
|
15
|
+
assert.equal(getSkill('quick-bytes')?.id, 'agentsam-app-fundamentals');
|
|
16
|
+
assert.equal(getSkill('no-regress')?.id, 'agentsam-progression-guard');
|
|
17
|
+
|
|
18
|
+
const fundamentals = loadSkill('quick-bytes', { references: true });
|
|
19
|
+
assert.equal(fundamentals.references.length, 2);
|
|
20
|
+
assert.match(fundamentals.instructions, /Credentialed-destination card/);
|
|
21
|
+
assert.match(fundamentals.instructions, /AST/);
|
|
22
|
+
assert.match(fundamentals.instructions, /Merkle/);
|
|
23
|
+
assert.match(fundamentals.instructions, /Firewall\/WAF/);
|
|
24
|
+
assert.match(fundamentals.references[0].content, /OAuth redirect mental model/);
|
|
25
|
+
assert.match(fundamentals.references[1].content, /The codebase is a graph/);
|
|
26
|
+
|
|
27
|
+
const guard = loadSkill('no-regress', { references: true });
|
|
28
|
+
assert.equal(guard.references.length, 2);
|
|
29
|
+
assert.match(guard.instructions, /failed candidate must not advance the last-known-good baseline/);
|
|
30
|
+
assert.match(guard.instructions, /agentsam deploy-receipt capture/);
|
|
31
|
+
assert.match(guard.references[0].content, /Gate 6 — postdeploy/);
|
|
32
|
+
assert.match(guard.references[1].content, /Ingress versus egress hooks/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('skills CLI lists compact cards and loads quick reminders by alias', () => {
|
|
36
|
+
const listed = spawnSync(process.execPath, [cli, 'skills', '--json'], { cwd: root, encoding: 'utf8' });
|
|
37
|
+
assert.equal(listed.status, 0, listed.stderr);
|
|
38
|
+
const catalog = JSON.parse(listed.stdout);
|
|
39
|
+
assert.ok(catalog.some((skill) => skill.id === 'agentsam-app-fundamentals'));
|
|
40
|
+
assert.ok(catalog.some((skill) => skill.id === 'agentsam-progression-guard'));
|
|
41
|
+
|
|
42
|
+
const loaded = spawnSync(process.execPath, [cli, 'skills', 'quick-bytes'], { cwd: root, encoding: 'utf8' });
|
|
43
|
+
assert.equal(loaded.status, 0, loaded.stderr);
|
|
44
|
+
assert.match(loaded.stdout, /# AgentSam Application Fundamentals/);
|
|
45
|
+
assert.doesNotMatch(loaded.stdout, /# Trust, Credentials, and Credentialed Destinations/);
|
|
46
|
+
|
|
47
|
+
const full = spawnSync(process.execPath, [cli, 'skills', 'no-regress', '--references'], { cwd: root, encoding: 'utf8' });
|
|
48
|
+
assert.equal(full.status, 0, full.stderr);
|
|
49
|
+
assert.match(full.stdout, /# AgentSam Progression Guard/);
|
|
50
|
+
assert.match(full.stdout, /# Checkpoint Chain/);
|
|
51
|
+
assert.match(full.stdout, /# Hooks and Operational I\/O/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('portable npm package still owns all skill files', () => {
|
|
55
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
56
|
+
assert.ok(pkg.files.includes('skills'));
|
|
57
|
+
for (const rel of [
|
|
58
|
+
'skills/agentsam-app-fundamentals/SKILL.md',
|
|
59
|
+
'skills/agentsam-progression-guard/SKILL.md',
|
|
60
|
+
]) assert.equal(fs.existsSync(path.join(root, rel)), true, `${rel} must exist`);
|
|
61
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
const productApps = ['local-studio', 'cad-creator', 'client-cms-editor'];
|
|
9
|
+
const at = (...parts) => path.join(root, ...parts);
|
|
10
|
+
const exists = (...parts) => fs.existsSync(at(...parts));
|
|
11
|
+
|
|
12
|
+
function packageJson(...parts) {
|
|
13
|
+
return JSON.parse(fs.readFileSync(at(...parts, 'package.json'), 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('product apps are self-contained npm workspace roots with one lockfile', () => {
|
|
17
|
+
for (const app of productApps) {
|
|
18
|
+
const base = ['apps', app];
|
|
19
|
+
assert.ok(exists(...base, 'package.json'), `${app}: root package.json`);
|
|
20
|
+
assert.ok(exists(...base, 'package-lock.json'), `${app}: root package-lock.json`);
|
|
21
|
+
assert.ok(exists(...base, 'frontend', 'package.json'), `${app}: frontend package`);
|
|
22
|
+
assert.ok(exists(...base, 'backend', 'package.json'), `${app}: backend package`);
|
|
23
|
+
assert.equal(exists(...base, 'frontend', 'package-lock.json'), false, `${app}: no frontend lockfile`);
|
|
24
|
+
assert.equal(exists(...base, 'backend', 'package-lock.json'), false, `${app}: no backend lockfile`);
|
|
25
|
+
|
|
26
|
+
const pkg = packageJson(...base);
|
|
27
|
+
assert.deepEqual(pkg.workspaces, ['frontend', 'backend', 'shared/*'], `${app}: canonical workspace list`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('every product app owns its Cloudflare boundary under backend/', () => {
|
|
32
|
+
for (const app of productApps) {
|
|
33
|
+
const base = ['apps', app, 'backend'];
|
|
34
|
+
assert.ok(exists(...base, 'worker', 'index.js'), `${app}: backend/worker/index.js`);
|
|
35
|
+
assert.ok(exists(...base, 'wrangler.jsonc'), `${app}: backend/wrangler.jsonc`);
|
|
36
|
+
const wrangler = fs.readFileSync(at(...base, 'wrangler.jsonc'), 'utf8');
|
|
37
|
+
assert.match(wrangler, /"main"\s*:\s*"worker\/index\.js"/, `${app}: wrangler main`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('SDK root is package tooling only and does not own an app Worker', () => {
|
|
42
|
+
assert.equal(exists('wrangler.jsonc'), false);
|
|
43
|
+
assert.equal(exists('worker', 'index.js'), false);
|
|
44
|
+
assert.equal(exists('scripts', 'with-cloudflare-env.sh'), false);
|
|
45
|
+
|
|
46
|
+
const rootPkg = packageJson();
|
|
47
|
+
for (const workspace of rootPkg.workspaces || []) {
|
|
48
|
+
assert.equal(workspace.startsWith('apps/'), false, `root workspace must not absorb app workspace: ${workspace}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('Local Studio retired donor-era duplicate deployment configs', () => {
|
|
53
|
+
for (const file of ['wrangler.ui.toml', 'wrangler.workmode.toml', 'wrangler.toml.example']) {
|
|
54
|
+
assert.equal(exists('apps', 'local-studio', 'backend', file), false, `retire ${file}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
7
|
import { getCapability, getCapabilityManifest, repositorySnapshot } from '../src/capabilities/index.js';
|
|
8
8
|
import { getPreset, resolvePreset } from '../src/presets/index.js';
|
|
9
|
+
import { scanTrustBoundary } from '../src/security/trust-boundary.js';
|
|
9
10
|
|
|
10
11
|
const CLI = path.resolve('src/cli.js');
|
|
11
12
|
|
|
@@ -51,11 +52,51 @@ test('repository.snapshot composes deterministic evidence and content addressing
|
|
|
51
52
|
assert.equal(one.snapshot_id, two.snapshot_id);
|
|
52
53
|
assert.equal(one.repository.revision_sha, git(root, ['rev-parse', 'HEAD']));
|
|
53
54
|
assert.ok(one.tree.merkle_root);
|
|
55
|
+
assert.match(one.tree.metadata_root, /^sha256:[a-f0-9]{64}$/);
|
|
56
|
+
assert.equal(one.tree.manifest.format, 'agentsam-merkle');
|
|
57
|
+
assert.equal(one.tree.manifest.version, 1);
|
|
58
|
+
assert.equal(one.tree.manifest.hash_algorithm, 'sha256');
|
|
59
|
+
assert.match(one.tree.manifest.policy_hash, /^sha256:[a-f0-9]{64}$/);
|
|
60
|
+
assert.equal(one.tree.classifier.format, 'agentsam-filemeta');
|
|
61
|
+
assert.equal(one.tree.classifier.version, 1);
|
|
62
|
+
const source = one.tree.files.find((entry) => entry.path === 'src/index.js');
|
|
63
|
+
assert.equal(source.mode, 420);
|
|
64
|
+
assert.equal(source.system, 'snapshot-fixture');
|
|
65
|
+
assert.equal(source.language, 'javascript');
|
|
66
|
+
assert.deepEqual(source.symbols, ['value']);
|
|
67
|
+
assert.equal(one.analysis.trust_boundary.analyzer, 'agentsam-execution-boundary');
|
|
68
|
+
assert.equal(one.analysis.trust_boundary.metadata_root, one.tree.metadata_root);
|
|
69
|
+
assert.equal(one.analysis.trust_boundary.complete, true);
|
|
70
|
+
assert.equal(one.analysis.trust_boundary.status, 'clean');
|
|
54
71
|
assert.ok(one.intelligence.summary.file_count >= 2);
|
|
55
72
|
assert.equal(one.packages[0].name, 'snapshot-fixture');
|
|
56
73
|
assert.equal(one.knowledge.configured, false);
|
|
57
74
|
});
|
|
58
75
|
|
|
76
|
+
test('repository inspection and security scan share one Git-ignore/Merkle trust authority', async t => {
|
|
77
|
+
const root = fixture();
|
|
78
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
79
|
+
fs.mkdirSync(path.join(root, 'frontend'));
|
|
80
|
+
fs.mkdirSync(path.join(root, 'backend'));
|
|
81
|
+
fs.mkdirSync(path.join(root, 'ignored'));
|
|
82
|
+
fs.writeFileSync(path.join(root, 'frontend', 'client.ts'), "import { secretValue } from '../backend/secrets.ts';\nexport const value = secretValue;\n");
|
|
83
|
+
fs.writeFileSync(path.join(root, 'backend', 'secrets.ts'), "export const secretValue = process.env.API_KEY;\n");
|
|
84
|
+
fs.writeFileSync(path.join(root, 'ignored', 'noise.ts'), "import fs from 'node:fs'; export const x = process.env.PRIVATE_TOKEN + String(fs);\n");
|
|
85
|
+
fs.writeFileSync(path.join(root, '.gitignore'), 'ignored/\n');
|
|
86
|
+
|
|
87
|
+
const snapshot = await repositorySnapshot({ cwd: root, churnDays: 30 });
|
|
88
|
+
const boundary = await scanTrustBoundary(root);
|
|
89
|
+
assert.equal(snapshot.tree.metadata_root, boundary.metadata_root);
|
|
90
|
+
assert.equal(snapshot.tree.merkle_root, boundary.merkle_root);
|
|
91
|
+
assert.equal(snapshot.analysis.trust_boundary.status, 'action-required');
|
|
92
|
+
assert.deepEqual(
|
|
93
|
+
snapshot.analysis.trust_boundary.findings.map((item) => [item.kind, item.source, item.target]),
|
|
94
|
+
boundary.findings.map((item) => [item.kind, item.source, item.target]),
|
|
95
|
+
);
|
|
96
|
+
assert.equal(snapshot.tree.paths.some((value) => value.startsWith('ignored/')), false);
|
|
97
|
+
assert.equal(boundary.findings.some((item) => item.source.startsWith('ignored/')), false);
|
|
98
|
+
});
|
|
99
|
+
|
|
59
100
|
test('product UX creates a preset project, adds a feature, and inspects before first commit', t => {
|
|
60
101
|
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-product-'));
|
|
61
102
|
t.after(() => fs.rmSync(parent, { recursive: true, force: true }));
|
|
@@ -65,10 +106,15 @@ test('product UX creates a preset project, adds a feature, and inspects before f
|
|
|
65
106
|
});
|
|
66
107
|
const project = path.join(parent, 'demo');
|
|
67
108
|
const config = JSON.parse(fs.readFileSync(path.join(project, '.agentsam', 'config.json'), 'utf8'));
|
|
68
|
-
assert.equal(config.
|
|
69
|
-
assert.equal(config.
|
|
70
|
-
assert.
|
|
71
|
-
assert.
|
|
109
|
+
assert.equal(config.schema_version, 2);
|
|
110
|
+
assert.equal(config.project.name, 'demo');
|
|
111
|
+
assert.equal(config.product.preset, 'cms');
|
|
112
|
+
assert.deepEqual(config.product.features, ['cms', 'knowledge']);
|
|
113
|
+
assert.ok(config.product.capabilities.includes('repository.snapshot'));
|
|
114
|
+
assert.match(config.repository.id, /^local:[0-9a-f-]{36}$/);
|
|
115
|
+
assert.equal(config.defaults.runtime, 'local');
|
|
116
|
+
assert.equal(config.defaults.model, 'auto');
|
|
117
|
+
assert.equal(fs.existsSync(path.join(project, 'agentsam.config.js')), false);
|
|
72
118
|
|
|
73
119
|
execFileSync(process.execPath, [CLI, 'add', 'knowledge', '--cwd', project, '--json'], { stdio: 'pipe' });
|
|
74
120
|
const features = JSON.parse(fs.readFileSync(path.join(project, '.agentsam', 'features.json'), 'utf8'));
|
|
@@ -79,6 +125,9 @@ test('product UX creates a preset project, adds a feature, and inspects before f
|
|
|
79
125
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
126
|
}));
|
|
81
127
|
assert.equal(snapshot.capability, 'repository.snapshot');
|
|
128
|
+
assert.equal(snapshot.repository.repository_id, config.repository.id);
|
|
129
|
+
assert.equal(snapshot.repository.identity_source, 'project-config');
|
|
82
130
|
assert.equal(snapshot.repository.revision_sha, null);
|
|
83
131
|
assert.ok(snapshot.tree.stats.files > 0);
|
|
132
|
+
assert.ok(snapshot.analysis?.trust_boundary);
|
|
84
133
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { CLI_PREFERENCES_SCHEMA, detectCliProject, readCliPreferences, writeCliPreferences } from '../src/lib/cli-preferences.js';
|
|
7
|
+
import { renderBootSummary } from '../src/ui/boot.js';
|
|
8
|
+
|
|
9
|
+
test('CLI preferences are project-local and explicitly non-authoritative for routing', () => {
|
|
10
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-cli-prefs-'));
|
|
11
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'demo-project' }));
|
|
12
|
+
const written = writeCliPreferences(root, { runtime: 'sandbox', terminal: 'zsh', modelPreference: 'ollama:qwen2.5-coder' });
|
|
13
|
+
assert.equal(written.schemaVersion, CLI_PREFERENCES_SCHEMA);
|
|
14
|
+
assert.equal(written.modelAuthority, 'preference-only');
|
|
15
|
+
assert.deepEqual(readCliPreferences(root), written);
|
|
16
|
+
|
|
17
|
+
const identity = detectCliProject(root);
|
|
18
|
+
assert.equal(identity.project, 'demo-project');
|
|
19
|
+
assert.equal(identity.root, root);
|
|
20
|
+
|
|
21
|
+
const summary = renderBootSummary({ identity, preferences: written });
|
|
22
|
+
assert.match(summary, /Agent Sam/);
|
|
23
|
+
assert.match(summary, /demo-project/);
|
|
24
|
+
assert.match(summary, /qwen2\.5-coder/);
|
|
25
|
+
});
|