@inneranimalmedia/agentsam-sdk 2.6.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
- package/docs/RELEASES.md +7 -7
- package/docs/SOURCE_ARCHITECTURE.md +58 -0
- package/docs/TEST_TIERS.md +26 -0
- package/migrations/runtime/0001_cli_runtime.sql +298 -0
- package/package.json +28 -7
- package/packages/agentsam-repository/README.md +15 -0
- package/packages/agentsam-repository/package.json +25 -0
- package/packages/agentsam-repository/src/contracts.js +113 -0
- package/packages/agentsam-repository/src/index.js +3 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
- package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
- package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
- package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
- package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
- package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
- package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
- package/packages/identity/package.json +1 -1
- package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
- package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
- package/protocol/repository/repository-contract.schema.json +24 -0
- package/protocol/repository/repository-dependency.schema.json +24 -0
- package/protocol/repository/repository-identity.schema.json +17 -0
- package/protocol/rpc/v1/common.proto +16 -0
- package/protocol/rpc/v1/errors.proto +35 -0
- package/protocol/rpc/v1/knowledge.proto +77 -0
- package/services/knowledge/package-lock.json +333 -0
- package/services/knowledge/package.json +5 -1
- package/src/agent/responses-runner.js +63 -35
- package/src/capabilities/repository-snapshot.js +3 -3
- package/src/cli.js +23 -6
- package/src/commands/context-economics.js +17 -2
- package/src/commands/context.js +1 -1
- package/src/commands/db.js +20 -3
- package/src/commands/env.js +90 -0
- package/src/commands/knowledge.js +12 -4
- package/src/commands/merkle-persist.js +30 -11
- package/src/commands/merkle.js +1 -1
- package/src/commands/models.js +123 -65
- package/src/commands/ollama.js +26 -0
- package/src/commands/preferences.js +53 -26
- package/src/commands/shell.js +236 -48
- package/src/errors/contract.js +236 -0
- package/src/errors/index.js +14 -0
- package/src/index.js +13 -1
- package/src/knowledge/service/auth.js +13 -0
- package/src/knowledge/service/grpc-client.js +115 -0
- package/src/knowledge/service/grpc-codec.js +237 -0
- package/src/knowledge/service/grpc-server.js +83 -0
- package/src/knowledge/service/job-engine.js +248 -0
- package/src/knowledge/service/server.js +87 -135
- package/src/knowledge/source.js +1 -1
- package/src/lib/cli-preferences.js +31 -4
- package/src/lib/deploy-receipt/index.js +2 -2
- package/src/lib/knowledge-docker.js +6 -3
- package/src/lib/local-sessions.js +23 -2
- package/src/lib/local-status.js +1 -1
- package/src/lib/project-config.js +1 -1
- package/src/lib/provider-credentials.js +105 -5
- package/src/lib/slash-commands.js +4 -3
- package/src/local/migrations.js +93 -0
- package/src/local/runtime-store.js +141 -0
- package/src/local/sqlite.js +2 -0
- package/src/local-pty/server.js +113 -51
- package/src/models/discovery.js +292 -0
- package/src/providers/anthropic-messages.js +192 -0
- package/src/providers/cloudflare-chat.js +183 -0
- package/src/providers/factory.js +69 -0
- package/src/providers/gemini-generate-content.js +208 -0
- package/src/providers/index.js +5 -0
- package/src/providers/ollama-chat.js +148 -0
- package/src/providers/openai-responses.js +226 -75
- package/src/repository/index.js +14 -2
- package/src/rpc/generated/common_grpc_pb.js +1 -0
- package/src/rpc/generated/common_pb.js +536 -0
- package/src/rpc/generated/errors_grpc_pb.js +1 -0
- package/src/rpc/generated/errors_pb.js +482 -0
- package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
- package/src/rpc/generated/knowledge_pb.js +2168 -0
- package/src/rpc/generated/package.json +3 -0
- package/src/security/trust-boundary.js +2 -2
- package/src/telemetry/events.js +4 -1
- package/src/ui/cli/activity.js +76 -0
- package/src/ui/cli/compaction.js +15 -0
- package/src/ui/cli/footer.js +39 -0
- package/src/ui/cli/help.js +192 -0
- package/src/ui/cli/plan.js +20 -0
- package/src/ui/cli/runtime-events.js +110 -0
- package/src/ui/cli/waiting.js +16 -0
- package/src/ui/merkle/render.js +1 -1
- package/test/cli/preferences-runtime.test.mjs +11 -0
- package/test/cli/runtime-ui.test.mjs +74 -0
- package/test/error-diagnostics.test.mjs +57 -1
- package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
- package/test/integration/cli-help.test.mjs +37 -0
- package/test/integration/knowledge-rpc.test.mjs +112 -0
- package/test/integration/merkle-cli.test.mjs +61 -0
- package/test/integration/merkle-persistence-identity.test.mjs +48 -0
- package/test/integration/provider-env-cli.test.mjs +49 -0
- package/test/integration/provider-factory.test.mjs +197 -0
- package/test/integration/repository-company-graph.test.mjs +90 -0
- package/test/integration/runtime-migrations.test.mjs +82 -0
- package/test/knowledge-service.test.mjs +5 -0
- package/test/knowledge.test.mjs +16 -0
- package/test/live/terminal-transport.live.test.mjs +24 -0
- package/test/local-sessions.test.mjs +7 -1
- package/test/models.test.mjs +101 -4
- package/test/ollama.test.mjs +21 -0
- package/test/portable-context.test.mjs +1 -1
- package/test/provider-credentials.test.mjs +45 -1
- package/test/release-hygiene.test.mjs +13 -5
- package/test/responses-runner.test.mjs +3 -1
- package/test/shell.test.mjs +50 -8
- package/test/terminal/local-pty.mock.test.mjs +151 -0
- /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { buildMerkleTree } from '
|
|
3
|
-
import { gitIgnoredPaths } from '
|
|
2
|
+
import { buildMerkleTree } from '../../packages/agentsam-repository/src/merkle/index.js';
|
|
3
|
+
import { gitIgnoredPaths } from '../../packages/agentsam-repository/src/merkle/git-ignore.js';
|
|
4
4
|
import { analyzeExecutionBoundaries } from '../indexing/execution-boundary.js';
|
|
5
5
|
|
|
6
6
|
export async function scanTrustBoundary(projectRoot = process.cwd(), options = {}) {
|
package/src/telemetry/events.js
CHANGED
|
@@ -5,7 +5,10 @@ export const AGENT_EVENT_TYPES = Object.freeze([
|
|
|
5
5
|
'usage.snapshot', 'cost.snapshot',
|
|
6
6
|
'context.snapshot', 'context.compaction.started', 'context.compaction.completed',
|
|
7
7
|
'tool.search', 'tool.started', 'tool.completed', 'tool.failed',
|
|
8
|
-
'
|
|
8
|
+
'approval.requested', 'approval.resolved',
|
|
9
|
+
'plan.updated', 'task.updated',
|
|
10
|
+
'timer.started', 'timer.updated', 'timer.completed',
|
|
11
|
+
'runtime.waiting_input',
|
|
9
12
|
]);
|
|
10
13
|
|
|
11
14
|
function integer(value, label) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
const CLEAR_LINE = '\x1b[2K';
|
|
4
|
+
const SPINNER = ['◐', '◓', '◑', '◒'];
|
|
5
|
+
|
|
6
|
+
function elapsed(ms) {
|
|
7
|
+
if (ms < 1000) return `${Math.max(0, Math.round(ms))}ms`;
|
|
8
|
+
return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createInlineActivity(options = {}) {
|
|
12
|
+
const write = options.write || process.stdout.write.bind(process.stdout);
|
|
13
|
+
const interactive = options.interactive ?? Boolean(process.stdout?.isTTY);
|
|
14
|
+
const now = options.now || (() => Date.now());
|
|
15
|
+
const setTimer = options.setInterval || globalThis.setInterval;
|
|
16
|
+
const clearTimer = options.clearInterval || globalThis.clearInterval;
|
|
17
|
+
const intervalMs = Math.max(80, Number(options.intervalMs) || 120);
|
|
18
|
+
|
|
19
|
+
let label = String(options.label || 'Thinking');
|
|
20
|
+
let startedAt = 0;
|
|
21
|
+
let tick = 0;
|
|
22
|
+
let timer = null;
|
|
23
|
+
let active = false;
|
|
24
|
+
|
|
25
|
+
function frame() {
|
|
26
|
+
if (!active || !interactive) return;
|
|
27
|
+
const icon = pc.cyan(SPINNER[tick % SPINNER.length]);
|
|
28
|
+
write(`\r${CLEAR_LINE} ${icon} ${label} ${pc.dim('· ' + elapsed(now() - startedAt))}`);
|
|
29
|
+
tick += 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function start(nextLabel = label) {
|
|
33
|
+
if (active) {
|
|
34
|
+
update(nextLabel);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
label = String(nextLabel || label);
|
|
38
|
+
startedAt = now();
|
|
39
|
+
active = true;
|
|
40
|
+
if (interactive) {
|
|
41
|
+
frame();
|
|
42
|
+
timer = setTimer(frame, intervalMs);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function update(nextLabel) {
|
|
47
|
+
if (nextLabel) label = String(nextLabel);
|
|
48
|
+
if (!active) start(label);
|
|
49
|
+
else frame();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function finish(status = 'success', finalLabel = label) {
|
|
53
|
+
if (!active) return;
|
|
54
|
+
if (timer) clearTimer(timer);
|
|
55
|
+
timer = null;
|
|
56
|
+
const duration = elapsed(now() - startedAt);
|
|
57
|
+
if (interactive) write(`\r${CLEAR_LINE}`);
|
|
58
|
+
const icon = status === 'error' ? pc.red('✗') : pc.green('✓');
|
|
59
|
+
write(` ${icon} ${finalLabel} ${pc.dim('· ' + duration)}\n`);
|
|
60
|
+
active = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
get active() { return active; },
|
|
65
|
+
start,
|
|
66
|
+
update,
|
|
67
|
+
succeed(labelText) { finish('success', labelText || label); },
|
|
68
|
+
fail(labelText) { finish('error', labelText || 'Failed'); },
|
|
69
|
+
clear() {
|
|
70
|
+
if (timer) clearTimer(timer);
|
|
71
|
+
timer = null;
|
|
72
|
+
if (interactive && active) write(`\r${CLEAR_LINE}`);
|
|
73
|
+
active = false;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
function n(value) {
|
|
4
|
+
const number = Number(value);
|
|
5
|
+
return Number.isFinite(number) ? Math.max(0, Math.round(number)).toLocaleString('en-US') : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function renderCompactionReceipt(payload = {}) {
|
|
9
|
+
const before = n(payload.tokens_before ?? payload.before_tokens);
|
|
10
|
+
const after = n(payload.tokens_after ?? payload.after_tokens);
|
|
11
|
+
const duration = Number(payload.duration_ms);
|
|
12
|
+
const detail = before && after ? ` ${before} → ${after}` : '';
|
|
13
|
+
const timing = Number.isFinite(duration) ? ` · ${duration < 1000 ? Math.round(duration) + 'ms' : (duration / 1000).toFixed(1) + 's'}` : '';
|
|
14
|
+
return ` ${pc.green('↻')} Context compacted${pc.dim(detail + timing)}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
function count(value) {
|
|
4
|
+
const n = Number(value || 0);
|
|
5
|
+
if (!Number.isFinite(n)) return '0';
|
|
6
|
+
if (Math.abs(n) >= 1_000_000) {
|
|
7
|
+
const scaled = n / 1_000_000;
|
|
8
|
+
return (Number.isInteger(scaled) ? String(scaled) : scaled.toFixed(Math.abs(n) >= 10_000_000 ? 0 : 1)) + 'm';
|
|
9
|
+
}
|
|
10
|
+
if (Math.abs(n) >= 1_000) {
|
|
11
|
+
const scaled = n / 1_000;
|
|
12
|
+
return (Number.isInteger(scaled) ? String(scaled) : scaled.toFixed(Math.abs(n) >= 100_000 ? 0 : 1)) + 'k';
|
|
13
|
+
}
|
|
14
|
+
return String(Math.round(n));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ctxLabel(usage) {
|
|
18
|
+
const active = Number(usage?.current_context?.input_tokens || 0);
|
|
19
|
+
const window = Number(usage?.current_context?.window_tokens || 0);
|
|
20
|
+
if (!(window > 0)) return active > 0 ? `ctx ${count(active)} / unknown` : 'ctx unknown';
|
|
21
|
+
return `ctx ${Math.min(999, Math.round((active / window) * 100))}%`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function renderCliFooter(value = {}) {
|
|
25
|
+
const usage = value.usage || value.usageSnapshot || {};
|
|
26
|
+
const cumulative = usage.cumulative || {};
|
|
27
|
+
const model = String(value.model || value.modelLabel || 'model');
|
|
28
|
+
const tier = String(value.tier || '').trim();
|
|
29
|
+
const elapsedMs = Number(value.elapsedMs);
|
|
30
|
+
const parts = [
|
|
31
|
+
model,
|
|
32
|
+
ctxLabel(usage),
|
|
33
|
+
`↑${count(cumulative.input_tokens)} ↓${count(cumulative.output_tokens)}`,
|
|
34
|
+
];
|
|
35
|
+
if (Number(cumulative.cached_input_tokens) > 0) parts.push(`cache ${count(cumulative.cached_input_tokens)}`);
|
|
36
|
+
if (tier && tier !== 'default') parts.push(tier);
|
|
37
|
+
if (Number.isFinite(elapsedMs) && elapsedMs >= 0) parts.push(elapsedMs < 60_000 ? `${(elapsedMs / 1000).toFixed(1)}s` : `${Math.floor(elapsedMs / 60_000)}m${Math.floor((elapsedMs % 60_000) / 1000)}s`);
|
|
38
|
+
return pc.dim(' ' + parts.join(' · '));
|
|
39
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { isCancel, select } from '@clack/prompts';
|
|
3
|
+
import pkg from '../../../package.json' with { type: 'json' };
|
|
4
|
+
|
|
5
|
+
const HELP_TOPICS = Object.freeze([
|
|
6
|
+
{
|
|
7
|
+
id: 'start',
|
|
8
|
+
label: 'Start / resume',
|
|
9
|
+
summary: 'Enter Agent Sam, resume work, inspect identity, and choose a model.',
|
|
10
|
+
rows: [
|
|
11
|
+
['agentsam', 'Enter the interactive Agent Sam experience'],
|
|
12
|
+
['agentsam resume [session]', 'Resume a saved Agent Sam session'],
|
|
13
|
+
['agentsam whoami', 'Show authenticated account and credential status'],
|
|
14
|
+
['agentsam models', 'Probe account-visible hosted/local models'],
|
|
15
|
+
['agentsam env init <provider>', 'Create a secure local provider profile'],
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'work',
|
|
20
|
+
label: 'Build / inspect',
|
|
21
|
+
summary: 'Repository intelligence and normal project work.',
|
|
22
|
+
rows: [
|
|
23
|
+
['agentsam inspect', 'Bounded repository index / authority view'],
|
|
24
|
+
['agentsam index', 'Incremental AST and optional embeddings'],
|
|
25
|
+
['agentsam search "query"', 'Search indexed code/text'],
|
|
26
|
+
['agentsam repo snapshot', 'Git composition/churn snapshot'],
|
|
27
|
+
['agentsam security', 'Dependency and trust-boundary scan'],
|
|
28
|
+
['agentsam merkle', 'Integrity snapshots and comparisons'],
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'runtime',
|
|
33
|
+
label: 'Runtime / terminal',
|
|
34
|
+
summary: 'Local, remote, sandbox, and deployment controls.',
|
|
35
|
+
rows: [
|
|
36
|
+
['agentsam status', 'Project, Git, DB, API, and PTY status'],
|
|
37
|
+
['agentsam start-local', 'Start the local PTY service'],
|
|
38
|
+
['agentsam connections', 'Inspect available execution connections'],
|
|
39
|
+
['agentsam tunnel', 'Expose local PTY only when remote access is wanted'],
|
|
40
|
+
['agentsam deploy', 'Graduate intentionally to cloud infrastructure'],
|
|
41
|
+
['agentsam cloudflare', 'Wrangler and Cloudflare runtime diagnostics'],
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'inside',
|
|
46
|
+
label: 'Inside Agent Sam',
|
|
47
|
+
summary: 'Commands available while the interactive session is running.',
|
|
48
|
+
rows: [
|
|
49
|
+
['/', 'Open the keyboard command picker'],
|
|
50
|
+
['/model', 'Choose model, reasoning, and processing tier'],
|
|
51
|
+
['/context', 'Show live context economics'],
|
|
52
|
+
['/usage', 'Show token/cost/session receipt'],
|
|
53
|
+
['/settings', 'Change runtime, terminal, and model policy'],
|
|
54
|
+
['/status', 'Show project/runtime health'],
|
|
55
|
+
['/help [topic]', 'Show in-session help'],
|
|
56
|
+
['/exit', 'Return to the host shell'],
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'create',
|
|
61
|
+
label: 'Create / extend',
|
|
62
|
+
summary: 'Scaffold and add reusable capabilities.',
|
|
63
|
+
rows: [
|
|
64
|
+
['agentsam create <name> --preset <preset>', 'Create a new AgentSam project'],
|
|
65
|
+
['agentsam add <capability>', 'Add a supported capability'],
|
|
66
|
+
['agentsam capabilities [id]', 'Inspect capability contracts'],
|
|
67
|
+
['agentsam skills [id]', 'Inspect packaged skills'],
|
|
68
|
+
['agentsam identity init', 'Add reusable identity surfaces'],
|
|
69
|
+
['agentsam dockerize', 'Build supported container targets'],
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const TOPIC_ALIASES = new Map([
|
|
75
|
+
['models', 'start'], ['model', 'start'], ['provider', 'start'], ['providers', 'start'], ['resume', 'start'],
|
|
76
|
+
['repo', 'work'], ['repository', 'work'], ['inspect', 'work'], ['index', 'work'], ['security', 'work'],
|
|
77
|
+
['terminal', 'runtime'], ['connections', 'runtime'], ['remote', 'runtime'], ['sandbox', 'runtime'], ['deploy', 'runtime'],
|
|
78
|
+
['slash', 'inside'], ['commands', 'inside'], ['session', 'inside'], ['usage', 'inside'], ['context', 'inside'],
|
|
79
|
+
['create', 'create'], ['scaffold', 'create'], ['capabilities', 'create'], ['skills', 'create'],
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
function clean(value) {
|
|
83
|
+
return value == null ? '' : String(value).trim().toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function padRows(rows) {
|
|
87
|
+
const width = Math.min(42, Math.max(...rows.map(([command]) => command.length), 0));
|
|
88
|
+
return rows.map(([command, description]) => ' ' + pc.cyan(command.padEnd(width)) + ' ' + pc.dim(description));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveHelpTopic(value) {
|
|
92
|
+
const query = clean(value);
|
|
93
|
+
if (!query) return null;
|
|
94
|
+
const direct = HELP_TOPICS.find((topic) => topic.id === query || clean(topic.label) === query);
|
|
95
|
+
if (direct) return direct;
|
|
96
|
+
const alias = TOPIC_ALIASES.get(query);
|
|
97
|
+
if (alias) return HELP_TOPICS.find((topic) => topic.id === alias) || null;
|
|
98
|
+
return HELP_TOPICS.find((topic) =>
|
|
99
|
+
topic.rows.some(([command, description]) => clean(command).includes(query) || clean(description).includes(query))) || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function renderHelpOverview(version, options = {}) {
|
|
103
|
+
const lines = [
|
|
104
|
+
'',
|
|
105
|
+
' ' + pc.bold('Agent Sam') + ' ' + pc.dim('v' + version),
|
|
106
|
+
' ' + pc.dim('Type normally to work with Agent Sam. Use help only when you need the map.'),
|
|
107
|
+
'',
|
|
108
|
+
' ' + pc.bold('Start'),
|
|
109
|
+
' ' + pc.cyan('agentsam') + ' ' + pc.dim('enter the interactive experience'),
|
|
110
|
+
' ' + pc.cyan('agentsam resume') + ' ' + pc.dim('continue saved work'),
|
|
111
|
+
' ' + pc.cyan('agentsam help <topic>') + ' ' + pc.dim('focused help'),
|
|
112
|
+
'',
|
|
113
|
+
' ' + pc.bold('Common'),
|
|
114
|
+
' ' + pc.cyan('agentsam inspect') + ' ' + pc.dim('understand this repository'),
|
|
115
|
+
' ' + pc.cyan('agentsam models') + ' ' + pc.dim('see account-visible models'),
|
|
116
|
+
' ' + pc.cyan('agentsam status') + ' ' + pc.dim('check project/runtime health'),
|
|
117
|
+
' ' + pc.cyan('agentsam security') + ' ' + pc.dim('scan dependency + trust boundaries'),
|
|
118
|
+
' ' + pc.cyan('agentsam deploy') + ' ' + pc.dim('graduate intentionally'),
|
|
119
|
+
'',
|
|
120
|
+
' ' + pc.dim('Inside Agent Sam: press / for the command picker. Ask the selected model for natural-language help at any time.'),
|
|
121
|
+
];
|
|
122
|
+
if (options.showTopics !== false) {
|
|
123
|
+
lines.push('', ' ' + pc.dim('Topics: ' + HELP_TOPICS.map((topic) => topic.id).join(' · ') + ' · all'));
|
|
124
|
+
}
|
|
125
|
+
lines.push('');
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function renderHelpTopic(topic, version) {
|
|
130
|
+
if (!topic) return renderHelpOverview(version);
|
|
131
|
+
return [
|
|
132
|
+
'',
|
|
133
|
+
' ' + pc.bold('Agent Sam') + ' ' + pc.dim('v' + version) + ' ' + pc.dim('·') + ' ' + pc.bold(topic.label),
|
|
134
|
+
' ' + pc.dim(topic.summary),
|
|
135
|
+
'',
|
|
136
|
+
...padRows(topic.rows),
|
|
137
|
+
'',
|
|
138
|
+
' ' + pc.dim('Tip: run agentsam help for the map, or agentsam to return to the interactive experience.'),
|
|
139
|
+
'',
|
|
140
|
+
].join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function renderAllHelp(version) {
|
|
144
|
+
const lines = [renderHelpOverview(version, { showTopics: false })];
|
|
145
|
+
for (const topic of HELP_TOPICS) lines.push(renderHelpTopic(topic, version));
|
|
146
|
+
return lines.join('');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function runHelp(argv = [], options = {}) {
|
|
150
|
+
const version = String(options.version || pkg.version || 'unknown');
|
|
151
|
+
const write = options.write || ((value) => process.stdout.write(value));
|
|
152
|
+
const args = argv.filter((arg) => arg !== '--interactive');
|
|
153
|
+
|
|
154
|
+
if (args.includes('--all')) {
|
|
155
|
+
write(renderAllHelp(version));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const topicArg = args.find((arg) => !arg.startsWith('-'));
|
|
160
|
+
if (topicArg) {
|
|
161
|
+
const topic = resolveHelpTopic(topicArg);
|
|
162
|
+
if (!topic) {
|
|
163
|
+
write(renderHelpOverview(version));
|
|
164
|
+
write(' ' + pc.yellow('No exact help topic matched') + ' ' + topicArg + '\n\n');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
write(renderHelpTopic(topic, version));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const interactive = options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
172
|
+
if (!interactive) {
|
|
173
|
+
write(renderHelpOverview(version));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const choice = await select({
|
|
178
|
+
message: 'Agent Sam help',
|
|
179
|
+
options: [
|
|
180
|
+
...HELP_TOPICS.map((topic) => ({ value: topic.id, label: topic.label, hint: topic.summary })),
|
|
181
|
+
{ value: 'all', label: 'All commands', hint: 'Print the complete deterministic help map' },
|
|
182
|
+
{ value: 'exit', label: 'Back', hint: 'Return without printing more help' },
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
if (isCancel(choice) || choice === 'exit') return;
|
|
186
|
+
if (choice === 'all') write(renderAllHelp(version));
|
|
187
|
+
else write(renderHelpTopic(resolveHelpTopic(choice), version));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function listHelpTopics() {
|
|
191
|
+
return HELP_TOPICS.map((topic) => ({ id: topic.id, label: topic.label, summary: topic.summary }));
|
|
192
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
export function renderPlanUpdate(payload = {}) {
|
|
4
|
+
const items = Array.isArray(payload.items) ? payload.items : Array.isArray(payload.todos) ? payload.todos : [];
|
|
5
|
+
if (!items.length) return '';
|
|
6
|
+
const lines = ['', ' ' + pc.bold(payload.title || 'Plan')];
|
|
7
|
+
for (const item of items.slice(0, 20)) {
|
|
8
|
+
const status = String(item.status || 'open');
|
|
9
|
+
const icon = status === 'complete' || status === 'completed' || status === 'done'
|
|
10
|
+
? pc.green('✓')
|
|
11
|
+
: status === 'running' || status === 'active'
|
|
12
|
+
? pc.cyan('●')
|
|
13
|
+
: status === 'blocked'
|
|
14
|
+
? pc.yellow('◆')
|
|
15
|
+
: pc.dim('○');
|
|
16
|
+
lines.push(` ${icon} ${item.title || item.label || item.id || 'task'}`);
|
|
17
|
+
}
|
|
18
|
+
lines.push('');
|
|
19
|
+
return lines.join('\n');
|
|
20
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { renderPlanUpdate } from './plan.js';
|
|
2
|
+
import { renderWaitingInput } from './waiting.js';
|
|
3
|
+
import { renderCompactionReceipt } from './compaction.js';
|
|
4
|
+
|
|
5
|
+
export const RUNTIME_EVENT_ENVELOPE_SCHEMA = 'agentsam-runtime-event-v1';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Transport-neutral event contract shared by standalone and future platform-connected runs.
|
|
9
|
+
* A platform SSE/WebSocket producer only needs to emit this normalized envelope; the SDK does
|
|
10
|
+
* not hardcode or invent the platform endpoint that will carry it.
|
|
11
|
+
*/
|
|
12
|
+
export function normalizeRuntimeEventEnvelope(value = {}) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
14
|
+
throw new TypeError('runtime event envelope must be an object');
|
|
15
|
+
}
|
|
16
|
+
const type = String(value.type || '').trim();
|
|
17
|
+
if (!type) throw new TypeError('runtime event envelope type is required');
|
|
18
|
+
const sequence = Number(value.sequence);
|
|
19
|
+
const envelope = {
|
|
20
|
+
schema: RUNTIME_EVENT_ENVELOPE_SCHEMA,
|
|
21
|
+
schema_version: Number.isInteger(Number(value.schema_version)) ? Number(value.schema_version) : 1,
|
|
22
|
+
type,
|
|
23
|
+
timestamp: String(value.timestamp || new Date().toISOString()),
|
|
24
|
+
...(value.run_id ? { run_id: String(value.run_id) } : {}),
|
|
25
|
+
...(Number.isInteger(sequence) && sequence >= 0 ? { sequence } : {}),
|
|
26
|
+
payload: value.payload && typeof value.payload === 'object' && !Array.isArray(value.payload)
|
|
27
|
+
? value.payload
|
|
28
|
+
: value.payload == null ? {} : { value: value.payload },
|
|
29
|
+
};
|
|
30
|
+
return Object.freeze(envelope);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function line(write, text) {
|
|
34
|
+
if (text) write(text.endsWith('\n') ? text : text + '\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createCliRuntimePresenter(options = {}) {
|
|
38
|
+
const activity = options.activity;
|
|
39
|
+
const write = options.write || process.stdout.write.bind(process.stdout);
|
|
40
|
+
const state = options.state || {};
|
|
41
|
+
|
|
42
|
+
function handle(event = {}) {
|
|
43
|
+
const envelope = normalizeRuntimeEventEnvelope(event);
|
|
44
|
+
const { type, payload } = envelope;
|
|
45
|
+
|
|
46
|
+
if (type === 'usage.snapshot') {
|
|
47
|
+
state.usageSnapshot = payload;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (type === 'cost.snapshot') {
|
|
51
|
+
state.costSnapshot = payload;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (type === 'model.started') {
|
|
55
|
+
activity?.update(`Thinking · ${payload.model || payload.provider || 'model'}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (type === 'tool.search') {
|
|
59
|
+
activity?.update('Finding relevant tools');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (type === 'tool.started') {
|
|
63
|
+
activity?.update(`Using ${payload.capability_id || payload.tool || 'tool'}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (type === 'tool.completed') {
|
|
67
|
+
activity?.update(`Tool complete · ${payload.capability_id || payload.tool || 'tool'}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (type === 'tool.failed') {
|
|
71
|
+
activity?.update(`Tool failed · ${payload.capability_id || payload.tool || 'tool'}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (type === 'context.compaction.started') {
|
|
75
|
+
state.compactionStartedAt = Date.now();
|
|
76
|
+
activity?.update('Compacting context');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (type === 'context.compaction.completed') {
|
|
80
|
+
const enriched = {
|
|
81
|
+
...payload,
|
|
82
|
+
duration_ms: payload.duration_ms ?? (state.compactionStartedAt ? Date.now() - state.compactionStartedAt : undefined),
|
|
83
|
+
};
|
|
84
|
+
state.lastCompaction = enriched;
|
|
85
|
+
activity?.update('Context ready');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (type === 'plan.updated') {
|
|
89
|
+
activity?.clear();
|
|
90
|
+
line(write, renderPlanUpdate(payload));
|
|
91
|
+
activity?.start('Working');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (type === 'runtime.waiting_input') {
|
|
95
|
+
activity?.clear();
|
|
96
|
+
line(write, renderWaitingInput(payload));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (type === 'run.failed' || type === 'error.observed') {
|
|
100
|
+
activity?.update('Handling error');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function compactionReceipt() {
|
|
105
|
+
if (!state.lastCompaction) return '';
|
|
106
|
+
return renderCompactionReceipt(state.lastCompaction);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Object.freeze({ handle, compactionReceipt, state });
|
|
110
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
export function renderWaitingInput(payload = {}) {
|
|
4
|
+
const reason = String(payload.reason || payload.awaiting_input_reason || 'input required');
|
|
5
|
+
const url = String(payload.url || payload.auth_url || '').trim();
|
|
6
|
+
const code = String(payload.code || payload.user_code || '').trim();
|
|
7
|
+
const lines = [
|
|
8
|
+
'',
|
|
9
|
+
` ${pc.yellow('◇')} ${pc.bold('Waiting for you')} ${pc.dim('· ' + reason)}`,
|
|
10
|
+
];
|
|
11
|
+
if (url) lines.push(` Open ${pc.cyan(url)}`);
|
|
12
|
+
if (code) lines.push(` Code ${pc.bold(code)}`);
|
|
13
|
+
lines.push(` ${pc.dim(payload.message || 'Complete the requested step; Agent Sam will continue when the runtime is ready.')}`);
|
|
14
|
+
lines.push('');
|
|
15
|
+
return lines.join('\n');
|
|
16
|
+
}
|
package/src/ui/merkle/render.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { comparePaths } from '
|
|
1
|
+
import { comparePaths } from '../../../packages/agentsam-repository/src/merkle/hash.js';
|
|
2
2
|
|
|
3
3
|
export const safeText = (value) => String(value ?? '').replace(/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g,
|
|
4
4
|
(char) => '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { runtimeOptions } from '../../src/commands/preferences.js';
|
|
4
|
+
|
|
5
|
+
test('standalone users only see local execution', () => {
|
|
6
|
+
assert.deepEqual(runtimeOptions({ accountConnected: false }).map((row) => row.value), ['local']);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('IAM-connected users can choose enrolled remote and sandbox lanes', () => {
|
|
10
|
+
assert.deepEqual(runtimeOptions({ accountConnected: true }).map((row) => row.value), ['local', 'remote', 'sandbox']);
|
|
11
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createInlineActivity } from '../../src/ui/cli/activity.js';
|
|
4
|
+
import { renderCliFooter } from '../../src/ui/cli/footer.js';
|
|
5
|
+
import { createCliRuntimePresenter, normalizeRuntimeEventEnvelope, RUNTIME_EVENT_ENVELOPE_SCHEMA } from '../../src/ui/cli/runtime-events.js';
|
|
6
|
+
import { renderWaitingInput } from '../../src/ui/cli/waiting.js';
|
|
7
|
+
|
|
8
|
+
test('CLI footer reports unknown context honestly', () => {
|
|
9
|
+
const text = renderCliFooter({
|
|
10
|
+
model: 'claude-test',
|
|
11
|
+
usageSnapshot: {
|
|
12
|
+
current_context: { input_tokens: 1234, window_tokens: 0 },
|
|
13
|
+
cumulative: { input_tokens: 5000, output_tokens: 600, cached_input_tokens: 2000 },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
assert.match(text, /claude-test/);
|
|
17
|
+
assert.match(text, /ctx 1\.2k \/ unknown/);
|
|
18
|
+
assert.match(text, /↑5k ↓600/);
|
|
19
|
+
assert.match(text, /cache 2k/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('runtime event envelope is the single standalone/platform producer contract', () => {
|
|
23
|
+
const envelope = normalizeRuntimeEventEnvelope({
|
|
24
|
+
schema_version: 1,
|
|
25
|
+
type: 'tool.started',
|
|
26
|
+
timestamp: '2026-09-18T00:00:00.000Z',
|
|
27
|
+
run_id: 'run_1',
|
|
28
|
+
sequence: 3,
|
|
29
|
+
payload: { capability_id: 'repo.inspect' },
|
|
30
|
+
});
|
|
31
|
+
assert.equal(envelope.schema, RUNTIME_EVENT_ENVELOPE_SCHEMA);
|
|
32
|
+
assert.equal(envelope.schema_version, 1);
|
|
33
|
+
assert.equal(envelope.type, 'tool.started');
|
|
34
|
+
assert.equal(envelope.run_id, 'run_1');
|
|
35
|
+
assert.equal(envelope.sequence, 3);
|
|
36
|
+
assert.deepEqual(envelope.payload, { capability_id: 'repo.inspect' });
|
|
37
|
+
assert.throws(() => normalizeRuntimeEventEnvelope({ payload: {} }), /type is required/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('runtime presenter turns events into one-line activity and waiting handoff', () => {
|
|
41
|
+
let output = '';
|
|
42
|
+
let now = 1_000;
|
|
43
|
+
const timers = [];
|
|
44
|
+
const activity = createInlineActivity({
|
|
45
|
+
write: (value) => { output += value; },
|
|
46
|
+
interactive: false,
|
|
47
|
+
now: () => now,
|
|
48
|
+
setInterval: (fn) => { timers.push(fn); return timers.length; },
|
|
49
|
+
clearInterval() {},
|
|
50
|
+
});
|
|
51
|
+
const state = {};
|
|
52
|
+
const presenter = createCliRuntimePresenter({ activity, write: (value) => { output += value; }, state });
|
|
53
|
+
activity.start('Thinking');
|
|
54
|
+
presenter.handle({ type: 'tool.started', payload: { capability_id: 'repo.inspect' } });
|
|
55
|
+
presenter.handle({ type: 'usage.snapshot', payload: { current_context: { input_tokens: 10, window_tokens: 100 }, cumulative: {} } });
|
|
56
|
+
presenter.handle({ type: 'context.compaction.started', payload: { provider: 'test' } });
|
|
57
|
+
now += 500;
|
|
58
|
+
presenter.handle({ type: 'context.compaction.completed', payload: { provider: 'test', tokens_before: 80, tokens_after: 20, summary_text: 'kept state' } });
|
|
59
|
+
presenter.handle({ type: 'runtime.waiting_input', payload: { reason: 'authentication', auth_url: 'https://example.test/auth', user_code: 'ABCD' } });
|
|
60
|
+
assert.equal(state.usageSnapshot.current_context.input_tokens, 10);
|
|
61
|
+
assert.equal(state.lastCompaction.tokens_before, 80);
|
|
62
|
+
assert.match(output, /Waiting for you/);
|
|
63
|
+
assert.match(output, /https:\/\/example\.test\/auth/);
|
|
64
|
+
assert.match(output, /ABCD/);
|
|
65
|
+
assert.match(presenter.compactionReceipt(), /80 → 20/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('waiting renderer gives CLI-native auth instructions', () => {
|
|
69
|
+
const text = renderWaitingInput({ awaiting_input_reason: 'otp', auth_url: 'https://example.test/login', user_code: '123456' });
|
|
70
|
+
assert.match(text, /Waiting for you/);
|
|
71
|
+
assert.match(text, /otp/);
|
|
72
|
+
assert.match(text, /Open/);
|
|
73
|
+
assert.match(text, /123456/);
|
|
74
|
+
});
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ERROR_CODE,
|
|
5
|
+
ERROR_REASON,
|
|
6
|
+
canonicalCodeFromGrpcStatus,
|
|
7
|
+
classifyCloudflareFailure,
|
|
8
|
+
classifyOAuthFailure,
|
|
9
|
+
classifyOpenAIError,
|
|
10
|
+
createErrorEnvelope,
|
|
11
|
+
createOpenAIHttpError,
|
|
12
|
+
diagnosticFromError,
|
|
13
|
+
grpcStatusForCode,
|
|
14
|
+
} from '../src/errors/index.js';
|
|
4
15
|
import { createOpenAIResponsesAdapter } from '../src/providers/openai-responses.js';
|
|
5
16
|
|
|
6
17
|
function response(status, body, headers = {}) { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...headers } }); }
|
|
@@ -57,3 +68,48 @@ test('Responses adapter throws a diagnostic error instead of flattening the prov
|
|
|
57
68
|
},
|
|
58
69
|
);
|
|
59
70
|
});
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
test('canonical error contract preserves transport-neutral code and reason mappings', () => {
|
|
74
|
+
const envelope = createErrorEnvelope({
|
|
75
|
+
code: ERROR_CODE.UNAVAILABLE,
|
|
76
|
+
reason: ERROR_REASON.TUNNEL_CONNECTOR_UNAVAILABLE,
|
|
77
|
+
message: 'Local terminal tunnel has no healthy connector.',
|
|
78
|
+
retryable: true,
|
|
79
|
+
http_status: 530,
|
|
80
|
+
provider: 'cloudflare',
|
|
81
|
+
provider_code: '1033',
|
|
82
|
+
transport: 'cloudflare_tunnel',
|
|
83
|
+
});
|
|
84
|
+
assert.equal(envelope.ok, false);
|
|
85
|
+
assert.equal(envelope.code, 'UNAVAILABLE');
|
|
86
|
+
assert.equal(envelope.reason, 'tunnel_connector_unavailable');
|
|
87
|
+
assert.equal(envelope.http_status, 530);
|
|
88
|
+
assert.equal(envelope.grpc_status, 14);
|
|
89
|
+
assert.equal(envelope.retryable, true);
|
|
90
|
+
assert.equal(canonicalCodeFromGrpcStatus(14), ERROR_CODE.UNAVAILABLE);
|
|
91
|
+
assert.equal(grpcStatusForCode(ERROR_CODE.UNAUTHENTICATED), 16);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('Cloudflare and OAuth boundary classifiers map native failures into canonical AgentSam semantics', () => {
|
|
95
|
+
assert.deepEqual(classifyCloudflareFailure({ httpStatus: 530, cloudflareCode: 1033 }), {
|
|
96
|
+
code: ERROR_CODE.UNAVAILABLE,
|
|
97
|
+
reason: ERROR_REASON.TUNNEL_CONNECTOR_UNAVAILABLE,
|
|
98
|
+
retryable: true,
|
|
99
|
+
});
|
|
100
|
+
assert.deepEqual(classifyCloudflareFailure({ httpStatus: 526 }), {
|
|
101
|
+
code: ERROR_CODE.UNAVAILABLE,
|
|
102
|
+
reason: ERROR_REASON.TLS_CERTIFICATE_INVALID,
|
|
103
|
+
retryable: false,
|
|
104
|
+
});
|
|
105
|
+
assert.deepEqual(classifyOAuthFailure('invalid_grant'), {
|
|
106
|
+
code: ERROR_CODE.UNAUTHENTICATED,
|
|
107
|
+
reason: ERROR_REASON.AUTH_INVALID,
|
|
108
|
+
retryable: false,
|
|
109
|
+
});
|
|
110
|
+
assert.deepEqual(classifyOAuthFailure('access_denied'), {
|
|
111
|
+
code: ERROR_CODE.PERMISSION_DENIED,
|
|
112
|
+
reason: ERROR_REASON.PERMISSION_DENIED,
|
|
113
|
+
retryable: false,
|
|
114
|
+
});
|
|
115
|
+
});
|