@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
|
@@ -3,8 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { getProjectName, tryReadProjectConfig } from './project-config.js';
|
|
5
5
|
|
|
6
|
-
export const CLI_PREFERENCES_SCHEMA = 'agentsam-cli-preferences-
|
|
7
|
-
export const
|
|
6
|
+
export const CLI_PREFERENCES_SCHEMA = 'agentsam-cli-preferences-v3';
|
|
7
|
+
export const LEGACY_CLI_PREFERENCES_SCHEMAS = new Set(['agentsam-cli-preferences-v1', 'agentsam-cli-preferences-v2']);
|
|
8
8
|
|
|
9
9
|
function readJson(filename) {
|
|
10
10
|
try { return JSON.parse(fs.readFileSync(filename, 'utf8')); }
|
|
@@ -43,16 +43,43 @@ export function detectCliProject(startDir = process.cwd()) {
|
|
|
43
43
|
|
|
44
44
|
export function cliPreferencesPath(root) { return path.join(path.resolve(root), '.agentsam', 'cli.json'); }
|
|
45
45
|
|
|
46
|
+
function safeModelSnapshot(value) {
|
|
47
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
48
|
+
const provider = String(value.provider || '').trim();
|
|
49
|
+
const providerModelId = String(value.provider_model_id || '').trim();
|
|
50
|
+
if (!provider || !providerModelId) return null;
|
|
51
|
+
return {
|
|
52
|
+
model_key: String(value.model_key || `${provider}:${providerModelId}`),
|
|
53
|
+
provider,
|
|
54
|
+
provider_model_id: providerModelId,
|
|
55
|
+
label: String(value.label || providerModelId),
|
|
56
|
+
availability: value.availability === 'available' ? 'available' : 'unverified',
|
|
57
|
+
availability_source: String(value.availability_source || ''),
|
|
58
|
+
context_window: Number.isFinite(Number(value.context_window)) && Number(value.context_window) > 0 ? Number(value.context_window) : null,
|
|
59
|
+
context_window_source: String(value.context_window_source || 'unknown'),
|
|
60
|
+
max_output_tokens: Number.isFinite(Number(value.max_output_tokens)) && Number(value.max_output_tokens) > 0 ? Number(value.max_output_tokens) : null,
|
|
61
|
+
max_output_tokens_source: String(value.max_output_tokens_source || 'unknown'),
|
|
62
|
+
reasoning_efforts: Array.isArray(value.reasoning_efforts) && value.reasoning_efforts.length ? value.reasoning_efforts.map(String) : ['auto'],
|
|
63
|
+
service_tiers: Array.isArray(value.service_tiers) && value.service_tiers.length ? value.service_tiers.map(String) : ['default'],
|
|
64
|
+
capabilities: value.capabilities && typeof value.capabilities === 'object' ? { ...value.capabilities } : {},
|
|
65
|
+
pricing: value.pricing && typeof value.pricing === 'object' ? { ...value.pricing } : null,
|
|
66
|
+
context_policy: value.context_policy && typeof value.context_policy === 'object' ? { ...value.context_policy } : null,
|
|
67
|
+
source: value.source && typeof value.source === 'object' ? { ...value.source } : null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
function normalizePreferences(value = {}) {
|
|
72
|
+
const modelSnapshot = safeModelSnapshot(value.modelSnapshot);
|
|
47
73
|
return {
|
|
48
74
|
schemaVersion: CLI_PREFERENCES_SCHEMA,
|
|
49
75
|
trustedDirectory: value.trustedDirectory === true,
|
|
50
76
|
runtime: value.runtime || 'local',
|
|
51
77
|
terminal: value.terminal || '',
|
|
52
78
|
modelPreference: value.modelPreference || 'auto',
|
|
79
|
+
modelSnapshot,
|
|
53
80
|
reasoningEffort: value.reasoningEffort || 'auto',
|
|
54
81
|
serviceTier: value.serviceTier || 'default',
|
|
55
|
-
modelAuthority: 'preference-only',
|
|
82
|
+
modelAuthority: modelSnapshot?.availability === 'available' ? 'provider-verified' : 'preference-only',
|
|
56
83
|
updatedAt: value.updatedAt || null,
|
|
57
84
|
};
|
|
58
85
|
}
|
|
@@ -60,7 +87,7 @@ function normalizePreferences(value = {}) {
|
|
|
60
87
|
export function readCliPreferences(root) {
|
|
61
88
|
const value = readJson(cliPreferencesPath(root));
|
|
62
89
|
if (!value) return null;
|
|
63
|
-
if (value.schemaVersion !== CLI_PREFERENCES_SCHEMA && value.schemaVersion
|
|
90
|
+
if (value.schemaVersion !== CLI_PREFERENCES_SCHEMA && !LEGACY_CLI_PREFERENCES_SCHEMAS.has(value.schemaVersion)) return null;
|
|
64
91
|
return normalizePreferences(value);
|
|
65
92
|
}
|
|
66
93
|
|
|
@@ -2,8 +2,8 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { randomBytes } from 'node:crypto';
|
|
5
|
-
import { buildMerkleTree, diffTrees, readSnapshot, validateSnapshot } from '
|
|
6
|
-
import { normalizePolicy } from '
|
|
5
|
+
import { buildMerkleTree, diffTrees, readSnapshot, validateSnapshot } from '../../../packages/agentsam-repository/src/merkle/index.js';
|
|
6
|
+
import { normalizePolicy } from '../../../packages/agentsam-repository/src/merkle/policy.js';
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_DEPLOY_EXCLUDES = Object.freeze([
|
|
9
9
|
'.agentsam/deploy-merkle',
|
|
@@ -7,12 +7,15 @@ import { CONFIG_PATH, defaultConfig, readConfig } from '../knowledge/config.js';
|
|
|
7
7
|
|
|
8
8
|
const sdkRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
9
9
|
const runtimeFiles = () => {
|
|
10
|
-
const files = ['services/knowledge/package.json', 'services/knowledge/package-lock.json', 'src/
|
|
10
|
+
const files = ['services/knowledge/package.json', 'services/knowledge/package-lock.json', 'packages/agentsam-repository/src/merkle/hash.js', 'src/errors/contract.js'];
|
|
11
11
|
const walk = dir => { for (const entry of fs.readdirSync(path.join(sdkRoot, dir), { withFileTypes: true })) {
|
|
12
12
|
const file = `${dir}/${entry.name}`;
|
|
13
|
-
if (entry.isDirectory()) walk(file);
|
|
13
|
+
if (entry.isDirectory()) walk(file);
|
|
14
|
+
else if (entry.isFile() && (/\.(js|sql)$/.test(entry.name) || file === 'src/rpc/generated/package.json')) files.push(file);
|
|
14
15
|
} };
|
|
15
|
-
walk('src/knowledge');
|
|
16
|
+
walk('src/knowledge');
|
|
17
|
+
walk('src/rpc/generated');
|
|
18
|
+
return files.sort();
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
export function generateKnowledgeDocker(opts = {}) {
|
|
@@ -52,17 +52,37 @@ function normalizeUsage(value = {}) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function normalizeCostBreakdown(value = {}) {
|
|
56
|
+
return {
|
|
57
|
+
input: Number(value.input || 0),
|
|
58
|
+
cached_input: Number(value.cached_input || 0),
|
|
59
|
+
cache_write: Number(value.cache_write || 0),
|
|
60
|
+
output: Number(value.output || 0),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function localSessionElapsedMs(session = {}, at = Date.now()) {
|
|
65
|
+
const accumulated = Math.max(0, Number(session.active_elapsed_ms || 0));
|
|
66
|
+
const startedAt = Date.parse(clean(session.active_started_at));
|
|
67
|
+
if (clean(session.status) !== 'active' || !Number.isFinite(startedAt)) return accumulated;
|
|
68
|
+
return accumulated + Math.max(0, Number(at) - startedAt);
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
export function normalizeLocalSession(value = {}) {
|
|
56
72
|
const createdAt = clean(value.created_at) || now();
|
|
73
|
+
const status = clean(value.status) || 'active';
|
|
74
|
+
const updatedAt = clean(value.updated_at) || createdAt;
|
|
57
75
|
return {
|
|
58
76
|
schema_version: LOCAL_SESSION_SCHEMA,
|
|
59
77
|
id: validateSessionId(value.id || createLocalSessionId()),
|
|
60
|
-
status
|
|
78
|
+
status,
|
|
61
79
|
cwd: path.resolve(clean(value.cwd) || process.cwd()),
|
|
62
80
|
title: clean(value.title) || sessionTitleFromInput(value.last_input),
|
|
63
81
|
last_input: clean(value.last_input) || null,
|
|
64
82
|
created_at: createdAt,
|
|
65
|
-
updated_at:
|
|
83
|
+
updated_at: updatedAt,
|
|
84
|
+
active_elapsed_ms: Math.max(0, Number(value.active_elapsed_ms || 0)),
|
|
85
|
+
active_started_at: clean(value.active_started_at) || (status === 'active' ? updatedAt : null),
|
|
66
86
|
model_key: clean(value.model_key) || null,
|
|
67
87
|
provider_model_id: clean(value.provider_model_id) || null,
|
|
68
88
|
reasoning_effort: clean(value.reasoning_effort) || null,
|
|
@@ -72,6 +92,7 @@ export function normalizeLocalSession(value = {}) {
|
|
|
72
92
|
usage_snapshot: value.usage_snapshot && typeof value.usage_snapshot === 'object' ? structuredClone(value.usage_snapshot) : null,
|
|
73
93
|
cumulative_usage: normalizeUsage(value.cumulative_usage || {}),
|
|
74
94
|
total_cost_usd: Number(value.total_cost_usd || 0),
|
|
95
|
+
cost_breakdown_usd: normalizeCostBreakdown(value.cost_breakdown_usd || {}),
|
|
75
96
|
approved_projected_call_cost_usd: Number(value.approved_projected_call_cost_usd || 0),
|
|
76
97
|
last_error: value.last_error && typeof value.last_error === 'object' ? structuredClone(value.last_error) : null,
|
|
77
98
|
};
|
package/src/lib/local-status.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { tryResolveGitContext } from '
|
|
3
|
+
import { tryResolveGitContext } from '../../packages/agentsam-repository/src/git-context.js';
|
|
4
4
|
import { inspectLocalSqlite } from '../local/sqlite.js';
|
|
5
5
|
import { getCreatedWithVersion, getDefaultProfile, getDeployTarget, getLocalDatabasePath, getProjectName, getProjectPreset, getRepositoryId, tryReadProjectConfig } from './project-config.js';
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { tryResolveGitContext } from '
|
|
4
|
+
import { tryResolveGitContext } from '../../packages/agentsam-repository/src/git-context.js';
|
|
5
5
|
|
|
6
6
|
export const PROJECT_CONFIG_PATH = '.agentsam/config.json';
|
|
7
7
|
export const PROJECT_CONFIG_SCHEMA_VERSION = 2;
|
|
@@ -7,10 +7,16 @@ const PROVIDER_CREDENTIALS = Object.freeze({
|
|
|
7
7
|
gemini: Object.freeze({ env: 'GEMINI_API_KEY', files: ['gemini.env'] }),
|
|
8
8
|
anthropic: Object.freeze({ env: 'ANTHROPIC_API_KEY', files: ['anthropic.env'] }),
|
|
9
9
|
grok: Object.freeze({ env: 'XAI_API_KEY', files: ['grok.env', 'xai.env'] }),
|
|
10
|
-
cloudflare: Object.freeze({ env: 'CLOUDFLARE_API_TOKEN', files: ['cloudflare.env'] }),
|
|
10
|
+
cloudflare: Object.freeze({ env: 'CLOUDFLARE_API_TOKEN', files: ['cloudflare.env'], accountEnv: ['ACCOUNT_ID', 'CLOUDFLARE_ACCOUNT_ID'] }),
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
14
|
+
function normalizeCloudflareAccountId(value) {
|
|
15
|
+
const id = clean(value);
|
|
16
|
+
if (!id) return '';
|
|
17
|
+
if (!/^[a-f0-9]{32}$/i.test(id)) throw new Error('invalid_cloudflare_account_id');
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
function homeDirectory(options = {}) {
|
|
16
22
|
return path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir());
|
|
@@ -36,6 +42,96 @@ function secureFile(filename) {
|
|
|
36
42
|
return { ok: true, mode: stat.mode & 0o777 };
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
function firstEnvValue(source, names = []) {
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
const value = clean(parseEnvValue(source, name));
|
|
48
|
+
if (value) return value;
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function firstRuntimeValue(env, names = []) {
|
|
54
|
+
for (const name of names) {
|
|
55
|
+
const value = clean(env?.[name]);
|
|
56
|
+
if (value) return value;
|
|
57
|
+
}
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function agentEnvDirectory(options = {}) {
|
|
62
|
+
return path.join(homeDirectory(options), '.agentsam', 'env.d');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function agentEnvLoaderPath(options = {}) {
|
|
66
|
+
return path.join(homeDirectory(options), '.agentsam', 'load-agent-env.sh');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ensureAgentEnvLoader(options = {}) {
|
|
70
|
+
const filename = agentEnvLoaderPath(options);
|
|
71
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true, mode: 0o700 });
|
|
72
|
+
const source = `# AgentSam provider environment loader. Source this file; do not execute it.
|
|
73
|
+
_agentsam_profile=\"\${1:-}\"
|
|
74
|
+
case \"\$_agentsam_profile\" in
|
|
75
|
+
openai|anthropic|gemini|grok|cloudflare) ;;
|
|
76
|
+
*) echo \"usage: source ~/.agentsam/load-agent-env.sh <openai|anthropic|gemini|grok|cloudflare>\" >&2; return 2 2>/dev/null || exit 2 ;;
|
|
77
|
+
esac
|
|
78
|
+
_agentsam_file=\"\${HOME}/.agentsam/env.d/\${_agentsam_profile}.env\"
|
|
79
|
+
if [ ! -f \"\$_agentsam_file\" ]; then
|
|
80
|
+
echo \"AgentSam provider profile not found: \$_agentsam_file\" >&2
|
|
81
|
+
return 1 2>/dev/null || exit 1
|
|
82
|
+
fi
|
|
83
|
+
set -a
|
|
84
|
+
. \"\$_agentsam_file\"
|
|
85
|
+
set +a
|
|
86
|
+
if [ \"\$_agentsam_profile\" = cloudflare ]; then
|
|
87
|
+
if [ -z \"\${ACCOUNT_ID:-}\" ] && [ -n \"\${CLOUDFLARE_ACCOUNT_ID:-}\" ]; then export ACCOUNT_ID=\"\$CLOUDFLARE_ACCOUNT_ID\"; fi
|
|
88
|
+
if [ -z \"\${CLOUDFLARE_ACCOUNT_ID:-}\" ] && [ -n \"\${ACCOUNT_ID:-}\" ]; then export CLOUDFLARE_ACCOUNT_ID=\"\$ACCOUNT_ID\"; fi
|
|
89
|
+
fi
|
|
90
|
+
unset _agentsam_file _agentsam_profile
|
|
91
|
+
`;
|
|
92
|
+
fs.writeFileSync(filename, source, { mode: 0o700 });
|
|
93
|
+
if (process.platform !== 'win32') fs.chmodSync(filename, 0o700);
|
|
94
|
+
return filename;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function profileTemplate(provider, options = {}) {
|
|
98
|
+
if (provider === 'cloudflare') {
|
|
99
|
+
const accountId = normalizeCloudflareAccountId(options.accountId);
|
|
100
|
+
return `# AgentSam Cloudflare profile\n# ACCOUNT_ID is your Cloudflare account identifier; it is not a secret.\nexport ACCOUNT_ID=\"${accountId}\"\nexport CLOUDFLARE_API_TOKEN=\"\"\n`;
|
|
101
|
+
}
|
|
102
|
+
const spec = PROVIDER_CREDENTIALS[provider];
|
|
103
|
+
if (!spec) throw new Error(`unsupported_provider:${provider}`);
|
|
104
|
+
return `# AgentSam ${provider} provider profile\nexport ${spec.env}=\"\"\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function ensureProviderEnvProfile(provider, options = {}) {
|
|
108
|
+
const spec = providerCredentialSpec(provider);
|
|
109
|
+
if (!spec) throw new Error(`unsupported_provider:${clean(provider).toLowerCase()}`);
|
|
110
|
+
const dir = agentEnvDirectory(options);
|
|
111
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
112
|
+
if (process.platform !== 'win32') fs.chmodSync(dir, 0o700);
|
|
113
|
+
const filename = path.join(dir, spec.files[0]);
|
|
114
|
+
let created = false;
|
|
115
|
+
if (!fs.existsSync(filename)) {
|
|
116
|
+
fs.writeFileSync(filename, profileTemplate(spec.provider, options), { mode: 0o600 });
|
|
117
|
+
created = true;
|
|
118
|
+
} else if (spec.provider === 'cloudflare' && clean(options.accountId)) {
|
|
119
|
+
const source = fs.readFileSync(filename, 'utf8');
|
|
120
|
+
const current = firstEnvValue(source, spec.accountEnv || []);
|
|
121
|
+
if (!current) {
|
|
122
|
+
const accountId = normalizeCloudflareAccountId(options.accountId);
|
|
123
|
+
const line = `export ACCOUNT_ID=\"${accountId}\"`;
|
|
124
|
+
const next = /^(?:export\s+)?ACCOUNT_ID=.*$/m.test(source)
|
|
125
|
+
? source.replace(/^(?:export\s+)?ACCOUNT_ID=.*$/m, line)
|
|
126
|
+
: `${line}\n${source}`;
|
|
127
|
+
fs.writeFileSync(filename, next, { mode: 0o600 });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (process.platform !== 'win32') fs.chmodSync(filename, 0o600);
|
|
131
|
+
const loader = ensureAgentEnvLoader(options);
|
|
132
|
+
return Object.freeze({ provider: spec.provider, file: filename, loader, created, source_command: `source ~/.agentsam/load-agent-env.sh ${spec.provider}` });
|
|
133
|
+
}
|
|
134
|
+
|
|
39
135
|
export function providerCredentialSpec(provider) {
|
|
40
136
|
const id = clean(provider).toLowerCase();
|
|
41
137
|
const spec = PROVIDER_CREDENTIALS[id];
|
|
@@ -46,18 +142,21 @@ export function resolveProviderCredential(provider, options = {}) {
|
|
|
46
142
|
const spec = providerCredentialSpec(provider);
|
|
47
143
|
if (!spec) return Object.freeze({ provider: clean(provider).toLowerCase(), configured: false, source: null, error: 'unsupported_provider', value: '' });
|
|
48
144
|
const env = options.env || process.env;
|
|
145
|
+
const accountIdFromEnv = firstRuntimeValue(env, spec.accountEnv || []);
|
|
49
146
|
const fromEnv = clean(env?.[spec.env]);
|
|
50
|
-
if (fromEnv) return Object.freeze({ provider: spec.provider, configured: true, source: 'environment', env: spec.env, file: null, error: null, value: fromEnv });
|
|
147
|
+
if (fromEnv) return Object.freeze({ provider: spec.provider, configured: true, source: 'environment', env: spec.env, file: null, error: null, value: fromEnv, account_id: accountIdFromEnv || null });
|
|
51
148
|
|
|
52
|
-
const dir =
|
|
149
|
+
const dir = agentEnvDirectory({ ...options, env });
|
|
53
150
|
for (const basename of spec.files) {
|
|
54
151
|
const filename = path.join(dir, basename);
|
|
55
152
|
if (!fs.existsSync(filename)) continue;
|
|
56
153
|
try {
|
|
57
154
|
const safety = secureFile(filename);
|
|
58
155
|
if (!safety.ok) return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: safety.error, value: '' });
|
|
59
|
-
const
|
|
60
|
-
|
|
156
|
+
const source = fs.readFileSync(filename, 'utf8');
|
|
157
|
+
const value = clean(parseEnvValue(source, spec.env));
|
|
158
|
+
const accountId = accountIdFromEnv || firstEnvValue(source, spec.accountEnv || []);
|
|
159
|
+
if (value) return Object.freeze({ provider: spec.provider, configured: true, source: 'agentsam_env_file', env: spec.env, file: filename, error: null, value, account_id: accountId || null });
|
|
61
160
|
return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: 'credential_variable_missing', value: '' });
|
|
62
161
|
} catch (error) {
|
|
63
162
|
return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: error?.message || String(error), value: '' });
|
|
@@ -75,6 +174,7 @@ export function describeProviderCredential(provider, options = {}) {
|
|
|
75
174
|
env: resolved.env || null,
|
|
76
175
|
file: resolved.file || null,
|
|
77
176
|
error: resolved.error || null,
|
|
177
|
+
account_id: resolved.account_id || null,
|
|
78
178
|
});
|
|
79
179
|
}
|
|
80
180
|
|
|
@@ -11,10 +11,11 @@ export const SLASH_COMMANDS = [
|
|
|
11
11
|
{ cmd: '/context', description: 'Show model context economics; add repo for Git bridge context', lane: 'context' },
|
|
12
12
|
{ cmd: '/status', description: 'Local project, DB, Git, and PTY health', lane: 'local' },
|
|
13
13
|
{ cmd: '/models', description: 'Probe providers and provider-verified known models', lane: 'model' },
|
|
14
|
-
{ cmd: '/login', description: '
|
|
15
|
-
{ cmd: '/logout', description: '
|
|
16
|
-
{ cmd: '/whoami', description: 'Show authenticated
|
|
14
|
+
{ cmd: '/login', description: 'Sign in to Inner Animal Media and save the machine-local Agent Sam session', lane: 'identity' },
|
|
15
|
+
{ cmd: '/logout', description: 'Sign out locally without deleting provider credentials', lane: 'identity' },
|
|
16
|
+
{ cmd: '/whoami', description: 'Show authenticated account identity and safe credential status', lane: 'identity' },
|
|
17
17
|
{ cmd: '/session', description: 'Show current session usage, cost, and resume receipt', lane: 'observability' },
|
|
18
|
+
{ cmd: '/usage', description: 'Show token usage, spend breakdown, and resume command', lane: 'observability' },
|
|
18
19
|
{ cmd: '/cf', description: 'Cloudflare native reads, Wrangler status, and CPU profile analysis', lane: 'cloudflare' },
|
|
19
20
|
{ cmd: '/settings', description: 'Choose project, runtime, terminal, and model policy' },
|
|
20
21
|
{ cmd: '/pwd', description: 'Print working directory', lane: 'terminal' },
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MIGRATIONS_DIR = path.resolve(
|
|
6
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
7
|
+
'..',
|
|
8
|
+
'..',
|
|
9
|
+
'migrations',
|
|
10
|
+
'runtime',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
function migrationFiles(dir) {
|
|
14
|
+
if (!fs.existsSync(dir)) return [];
|
|
15
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
16
|
+
.filter((entry) => entry.isFile() && /^\d+.*\.sql$/i.test(entry.name))
|
|
17
|
+
.map((entry) => entry.name)
|
|
18
|
+
.sort((a, b) => a.localeCompare(b));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureLedger(db) {
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS agentsam_schema_migrations (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
applied_at_unix INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
26
|
+
checksum TEXT
|
|
27
|
+
);
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checksumSource(source) {
|
|
32
|
+
let hash = 2166136261;
|
|
33
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
34
|
+
hash ^= source.charCodeAt(i);
|
|
35
|
+
hash = Math.imul(hash, 16777619);
|
|
36
|
+
}
|
|
37
|
+
return 'fnv1a32:' + (hash >>> 0).toString(16).padStart(8, '0');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function listAppliedMigrations(db) {
|
|
41
|
+
ensureLedger(db);
|
|
42
|
+
const result = await db.prepare(
|
|
43
|
+
'SELECT id, applied_at_unix, checksum FROM agentsam_schema_migrations ORDER BY id'
|
|
44
|
+
).all();
|
|
45
|
+
return result.results || [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function applyRuntimeMigrations(db, options = {}) {
|
|
49
|
+
const dir = path.resolve(options.migrationsDir || DEFAULT_MIGRATIONS_DIR);
|
|
50
|
+
ensureLedger(db);
|
|
51
|
+
const appliedRows = await listAppliedMigrations(db);
|
|
52
|
+
const applied = new Map(appliedRows.map((row) => [String(row.id), row]));
|
|
53
|
+
const results = [];
|
|
54
|
+
|
|
55
|
+
for (const filename of migrationFiles(dir)) {
|
|
56
|
+
const id = filename.replace(/\.sql$/i, '');
|
|
57
|
+
const source = fs.readFileSync(path.join(dir, filename), 'utf8');
|
|
58
|
+
const checksum = checksumSource(source);
|
|
59
|
+
const previous = applied.get(id);
|
|
60
|
+
|
|
61
|
+
if (previous) {
|
|
62
|
+
if (previous.checksum && previous.checksum !== checksum) {
|
|
63
|
+
throw new Error('migration_checksum_mismatch:' + id);
|
|
64
|
+
}
|
|
65
|
+
results.push({ id, filename, status: 'already_applied', checksum });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
db.exec('BEGIN IMMEDIATE');
|
|
70
|
+
try {
|
|
71
|
+
db.exec(source);
|
|
72
|
+
await db.prepare(
|
|
73
|
+
'INSERT INTO agentsam_schema_migrations (id, checksum) VALUES (?, ?)'
|
|
74
|
+
).bind(id, checksum).run();
|
|
75
|
+
db.exec('COMMIT');
|
|
76
|
+
results.push({ id, filename, status: 'applied', checksum });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
try { db.exec('ROLLBACK'); } catch {}
|
|
79
|
+
throw new Error('migration_failed:' + id + ':' + (error?.message || error));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
migrationsDir: dir,
|
|
85
|
+
total: results.length,
|
|
86
|
+
applied: results.filter((row) => row.status === 'applied').length,
|
|
87
|
+
results: Object.freeze(results),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function runtimeMigrationsDirectory() {
|
|
92
|
+
return DEFAULT_MIGRATIONS_DIR;
|
|
93
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { createLocalSqliteDatabase } from './sqlite.js';
|
|
4
|
+
import { applyRuntimeMigrations } from './migrations.js';
|
|
5
|
+
|
|
6
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
7
|
+
function hash(value) { return createHash('sha256').update(String(value || '')).digest('hex'); }
|
|
8
|
+
|
|
9
|
+
export function runtimeDatabasePath(cwd = process.cwd()) {
|
|
10
|
+
return path.join(path.resolve(cwd), '.agentsam', 'data', 'agentsam.sqlite');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function withStore(cwd, fn) {
|
|
14
|
+
const db = await createLocalSqliteDatabase(runtimeDatabasePath(cwd));
|
|
15
|
+
try {
|
|
16
|
+
await applyRuntimeMigrations(db);
|
|
17
|
+
return await fn(db);
|
|
18
|
+
} finally {
|
|
19
|
+
db.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createRuntimeRunId() {
|
|
24
|
+
return `arun_${randomUUID()}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function startRuntimeRun(value = {}) {
|
|
28
|
+
const id = clean(value.id) || createRuntimeRunId();
|
|
29
|
+
await withStore(value.cwd, async (db) => {
|
|
30
|
+
await db.prepare(`
|
|
31
|
+
INSERT INTO agentsam_agent_run (
|
|
32
|
+
id, account_id, source_client, surface, mode, model_key,
|
|
33
|
+
reasoning_effort, requested_service_tier, status, started_at_unix, updated_at_unix
|
|
34
|
+
) VALUES (?, ?, 'agentsam-cli', 'cli', ?, ?, ?, ?, 'running', unixepoch(), unixepoch())
|
|
35
|
+
`).bind(
|
|
36
|
+
id,
|
|
37
|
+
clean(value.account_id) || null,
|
|
38
|
+
clean(value.mode) || 'agent',
|
|
39
|
+
clean(value.model_key) || null,
|
|
40
|
+
clean(value.reasoning_effort) || null,
|
|
41
|
+
clean(value.service_tier) || null,
|
|
42
|
+
).run();
|
|
43
|
+
});
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function finishRuntimeRun(value = {}) {
|
|
48
|
+
if (!clean(value.id)) return;
|
|
49
|
+
await withStore(value.cwd, async (db) => {
|
|
50
|
+
const usage = value.usage || {};
|
|
51
|
+
await db.prepare(`
|
|
52
|
+
UPDATE agentsam_agent_run
|
|
53
|
+
SET status = ?,
|
|
54
|
+
actual_service_tier = COALESCE(?, actual_service_tier),
|
|
55
|
+
input_tokens = ?,
|
|
56
|
+
cached_input_tokens = ?,
|
|
57
|
+
output_tokens = ?,
|
|
58
|
+
reasoning_tokens = ?,
|
|
59
|
+
cost_usd = ?,
|
|
60
|
+
error_code = ?,
|
|
61
|
+
error_message = ?,
|
|
62
|
+
completed_at_unix = unixepoch(),
|
|
63
|
+
updated_at_unix = unixepoch(),
|
|
64
|
+
latency_ms = ?
|
|
65
|
+
WHERE id = ?
|
|
66
|
+
`).bind(
|
|
67
|
+
clean(value.status) || 'completed',
|
|
68
|
+
clean(value.actual_service_tier) || null,
|
|
69
|
+
Number(usage.input_tokens || 0),
|
|
70
|
+
Number(usage.cached_input_tokens || 0),
|
|
71
|
+
Number(usage.output_tokens || 0),
|
|
72
|
+
Number(usage.reasoning_tokens || 0),
|
|
73
|
+
Math.max(0, Number(value.cost_usd || 0)),
|
|
74
|
+
clean(value.error_code) || null,
|
|
75
|
+
clean(value.error_message) || null,
|
|
76
|
+
Number.isFinite(Number(value.latency_ms)) ? Math.max(0, Math.round(Number(value.latency_ms))) : null,
|
|
77
|
+
value.id,
|
|
78
|
+
).run();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function recordRuntimeCompaction(value = {}) {
|
|
83
|
+
if (!clean(value.agent_run_id)) return null;
|
|
84
|
+
const id = clean(value.id) || `cmp_${randomUUID()}`;
|
|
85
|
+
const summary = clean(value.summary_text);
|
|
86
|
+
const before = Math.max(0, Math.round(Number(value.tokens_before || 0)));
|
|
87
|
+
const after = Math.max(0, Math.round(Number(value.tokens_after || 0)));
|
|
88
|
+
const sourceHash = hash(summary || JSON.stringify(value.metadata || {}));
|
|
89
|
+
|
|
90
|
+
await withStore(value.cwd, async (db) => {
|
|
91
|
+
await db.prepare(`
|
|
92
|
+
INSERT INTO agentsam_compaction_events (
|
|
93
|
+
id, account_id, agent_run_id, compaction_type, compaction_scope,
|
|
94
|
+
compaction_strategy, source_kind, content_hash, provider, model_key,
|
|
95
|
+
tokens_before, tokens_after, status, summary_text, summary_json,
|
|
96
|
+
metrics_json, metadata_json, source_stored
|
|
97
|
+
) VALUES (?, ?, ?, 'context_summary', 'agent_run', 'summarize', ?, ?, ?, ?, ?, ?, 'completed', ?, ?, ?, ?, ?)
|
|
98
|
+
`).bind(
|
|
99
|
+
id,
|
|
100
|
+
clean(value.account_id) || null,
|
|
101
|
+
value.agent_run_id,
|
|
102
|
+
clean(value.source_kind) || 'api',
|
|
103
|
+
sourceHash,
|
|
104
|
+
clean(value.provider),
|
|
105
|
+
clean(value.model_key),
|
|
106
|
+
before,
|
|
107
|
+
after,
|
|
108
|
+
summary,
|
|
109
|
+
JSON.stringify({ summary }),
|
|
110
|
+
JSON.stringify({ tokens_before: before, tokens_after: after, tokens_saved: before - after }),
|
|
111
|
+
JSON.stringify(value.metadata || {}),
|
|
112
|
+
clean(value.source_stored) || 'local:agentsam_compaction_events',
|
|
113
|
+
).run();
|
|
114
|
+
|
|
115
|
+
if (summary) {
|
|
116
|
+
const digestId = `ctx_${randomUUID()}`;
|
|
117
|
+
const digestHash = hash(`${value.agent_run_id}:${id}:${summary}`);
|
|
118
|
+
await db.prepare(`
|
|
119
|
+
INSERT INTO agentsam_context_digest (
|
|
120
|
+
id, account_id, digest_type, agent_run_id, session_id,
|
|
121
|
+
source_hash, digest_hash, raw_size_bytes, reduced_size_bytes,
|
|
122
|
+
token_count, digest_text, generation_model, compaction_event_id
|
|
123
|
+
) VALUES (?, ?, 'session', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
124
|
+
`).bind(
|
|
125
|
+
digestId,
|
|
126
|
+
clean(value.account_id) || null,
|
|
127
|
+
value.agent_run_id,
|
|
128
|
+
clean(value.session_id) || null,
|
|
129
|
+
sourceHash,
|
|
130
|
+
digestHash,
|
|
131
|
+
Number(value.raw_size_bytes || 0) || null,
|
|
132
|
+
Buffer.byteLength(summary, 'utf8'),
|
|
133
|
+
after || Math.ceil(summary.length / 4),
|
|
134
|
+
summary,
|
|
135
|
+
clean(value.model_key) || null,
|
|
136
|
+
id,
|
|
137
|
+
).run();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return id;
|
|
141
|
+
}
|
package/src/local/sqlite.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { applyRuntimeMigrations } from './migrations.js';
|
|
3
4
|
|
|
4
5
|
async function loadSqlite() {
|
|
5
6
|
try {
|
|
@@ -77,6 +78,7 @@ export async function initializeLocalSqlite({
|
|
|
77
78
|
const db = await createLocalSqliteDatabase(resolvedDb);
|
|
78
79
|
try {
|
|
79
80
|
db.exec(schema);
|
|
81
|
+
await applyRuntimeMigrations(db);
|
|
80
82
|
} finally {
|
|
81
83
|
db.close();
|
|
82
84
|
}
|