@inneranimalmedia/agentsam-sdk 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/AGENTSAM.md +55 -0
  2. package/README.md +12 -8
  3. package/bin/agentsam +2 -0
  4. package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
  5. package/docs/CLI_SHELL.md +163 -53
  6. package/docs/RELEASES.md +16 -7
  7. package/package.json +20 -8
  8. package/packages/connectors/cloudflare/package.json +10 -0
  9. package/packages/connectors/cloudflare/src/index.js +127 -0
  10. package/packages/connectors/cloudflare/src/owner.js +76 -0
  11. package/packages/connectors/cloudflare/src/routes.js +223 -0
  12. package/packages/connectors/cloudflare/src/vault.js +80 -0
  13. package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
  14. package/packages/identity/package.json +2 -2
  15. package/packages/identity/src/contracts/auth-config.js +18 -7
  16. package/packages/identity/tests/auth-config.test.mjs +9 -5
  17. package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
  18. package/protocol/README.md +1 -0
  19. package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
  20. package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
  21. package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
  22. package/protocol/capabilities/manifest.json +47 -0
  23. package/protocol/context/context-budget.schema.json +10 -15
  24. package/protocol/context/context-item.schema.json +4 -5
  25. package/protocol/context/resolved-context-pack.schema.json +19 -14
  26. package/protocol/models/README.md +373 -0
  27. package/protocol/models/model-inventory-v2.schema.json +212 -0
  28. package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
  29. package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
  30. package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
  31. package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
  32. package/skills/catalog.json +18 -0
  33. package/src/agent/capability-adapter.js +25 -13
  34. package/src/agent/index.js +1 -0
  35. package/src/agent/responses-runner.js +325 -0
  36. package/src/cli.js +98 -28
  37. package/src/cloudflare/cpu-profile.js +115 -0
  38. package/src/cloudflare/index.js +14 -0
  39. package/src/cloudflare/wrangler.js +132 -0
  40. package/src/commands/account-auth.js +47 -0
  41. package/src/commands/cloudflare.js +58 -0
  42. package/src/commands/connections.js +93 -0
  43. package/src/commands/context-economics.js +114 -0
  44. package/src/commands/deploy.js +39 -3
  45. package/src/commands/eval.js +63 -0
  46. package/src/commands/interactive.js +2 -5
  47. package/src/commands/models.js +85 -40
  48. package/src/commands/preferences.js +101 -59
  49. package/src/commands/resume.js +67 -0
  50. package/src/commands/security.js +5 -3
  51. package/src/commands/shell.js +370 -109
  52. package/src/commands/tunnel.js +2 -2
  53. package/src/commands/whoami.js +86 -0
  54. package/src/context/budget.js +68 -6
  55. package/src/context/index.js +3 -1
  56. package/src/context/rehydrate.js +35 -0
  57. package/src/context/resolve.js +44 -12
  58. package/src/errors/diagnostic.js +160 -0
  59. package/src/errors/index.js +9 -0
  60. package/src/eval/context.js +191 -0
  61. package/src/eval/index.js +1 -0
  62. package/src/index.js +55 -1
  63. package/src/lib/account-session.js +98 -0
  64. package/src/lib/agent-instructions.js +73 -0
  65. package/src/lib/auth.js +4 -0
  66. package/src/lib/cli-preferences.js +28 -24
  67. package/src/lib/deploy/git-guard.js +69 -0
  68. package/src/lib/deploy/health.js +57 -0
  69. package/src/lib/deploy/local-studio.js +283 -0
  70. package/src/lib/deploy/secret-scan.js +65 -0
  71. package/src/lib/detect-context.js +2 -2
  72. package/src/lib/execution-approvals.js +59 -0
  73. package/src/lib/local-sessions.js +127 -0
  74. package/src/lib/provider-credentials.js +83 -0
  75. package/src/lib/scaffold/templates/worker-api/index.js +101 -20
  76. package/src/lib/scaffold/wizards/worker-api.js +27 -11
  77. package/src/lib/slash-commands.js +22 -16
  78. package/src/models/catalog.js +135 -0
  79. package/src/models/index.js +7 -0
  80. package/src/providers/index.js +5 -0
  81. package/src/providers/openai-responses.js +275 -0
  82. package/src/security/process.js +35 -9
  83. package/src/telemetry/contracts.js +203 -0
  84. package/src/telemetry/events.js +48 -0
  85. package/src/telemetry/index.js +8 -0
  86. package/src/tools/hydrate.js +35 -0
  87. package/src/tools/index.js +1 -0
  88. package/src/ui/boot.js +15 -17
  89. package/test/account-session.test.mjs +36 -0
  90. package/test/cli-preferences.test.mjs +26 -5
  91. package/test/cloudflare-connector.test.mjs +96 -0
  92. package/test/cloudflare-runtime.test.mjs +75 -0
  93. package/test/context.test.mjs +61 -12
  94. package/test/deploy-health-scan.test.mjs +67 -0
  95. package/test/error-diagnostics.test.mjs +59 -0
  96. package/test/eval-context.test.mjs +37 -0
  97. package/test/execution-approvals.test.mjs +27 -0
  98. package/test/local-sessions.test.mjs +42 -0
  99. package/test/local-studio-deploy.test.mjs +83 -0
  100. package/test/model-catalog.test.mjs +43 -0
  101. package/test/models.test.mjs +30 -16
  102. package/test/npm10-lock.test.mjs +29 -0
  103. package/test/openai-responses.test.mjs +95 -0
  104. package/test/provider-credentials.test.mjs +52 -0
  105. package/test/rehydrate.test.mjs +25 -0
  106. package/test/release-hygiene.test.mjs +4 -4
  107. package/test/responses-runner.test.mjs +148 -0
  108. package/test/shell.test.mjs +47 -20
  109. package/test/smoke.mjs +4 -1
  110. package/test/telemetry.test.mjs +79 -0
  111. package/test/tools-search.test.mjs +14 -1
  112. package/test/whoami-resume.test.mjs +56 -0
@@ -0,0 +1,191 @@
1
+ import { assessContextUsage, compactContextItem, createContextBudget, estimateContextTokens, rehydrateContextRef, resolveContext } from '../context/index.js';
2
+ import { calculateModelCost, getModelRecord } from '../models/index.js';
3
+ import { hydrateToolSchemas, searchToolCards } from '../tools/index.js';
4
+
5
+ const STRATEGIES = Object.freeze(['bounded', 'discovery', 'compact']);
6
+
7
+ const BASE_TOOLS = Object.freeze([
8
+ { name: 'repository.snapshot', description: 'Inspect repository identity, tree, packages, and structural evidence.', category: 'repository', input_schema: { type: 'object', properties: { cwd: { type: 'string' } } } },
9
+ { name: 'code.symbols', description: 'Find exact symbols, declarations, imports, and callers in indexed source.', category: 'code', input_schema: { type: 'object', required: ['query'], properties: { query: { type: 'string' } } } },
10
+ { name: 'files.read', description: 'Read a selected file or bounded source range by stable reference.', category: 'files', input_schema: { type: 'object', required: ['ref'], properties: { ref: { type: 'string' } } } },
11
+ { name: 'tests.trace', description: 'Trace a failing test to referenced source symbols and files.', category: 'tests', input_schema: { type: 'object', required: ['test'], properties: { test: { type: 'string' } } } },
12
+ { name: 'context.rehydrate', description: 'Recover previously compacted evidence by stable reference and hash.', category: 'context', input_schema: { type: 'object', required: ['ref'], properties: { ref: { type: 'string' } } } },
13
+ ...Array.from({ length: 20 }, (_, index) => ({ name: `misc.tool.${index}`, description: `Unrelated generic capability ${index}.`, category: 'misc', input_schema: { type: 'object', properties: {} } })),
14
+ ]);
15
+
16
+ const FIXTURES = Object.freeze({
17
+ 'exact-symbol-callers': {
18
+ task: 'Find the exact resolveContext implementation and its budget dependency before changing context selection.',
19
+ required: ['file:src/context/resolve.js', 'file:src/context/budget.js'],
20
+ syntheticBaseTokens: 8_000,
21
+ items: [
22
+ { ref: 'file:src/context/resolve.js', kind: 'file', priority: 100, content: 'resolveContext selects evidence, computes receipt composition, and defers overflow refs.'.repeat(40) },
23
+ { ref: 'file:src/context/budget.js', kind: 'file', priority: 95, content: 'createContextBudget separates model capacity from AgentSam working-set policy.'.repeat(36) },
24
+ { ref: 'file:README.md', kind: 'file', priority: 8, content: 'General product documentation.'.repeat(100) },
25
+ { ref: 'file:apps/local-studio/theme.css', kind: 'file', priority: 1, content: 'Unrelated visual styles.'.repeat(160) },
26
+ ],
27
+ },
28
+ 'two-file-repair': {
29
+ task: 'Repair a shell model-control regression spanning CLI preferences and slash dispatch without loading unrelated apps.',
30
+ required: ['file:src/lib/cli-preferences.js', 'file:src/commands/shell.js'],
31
+ syntheticBaseTokens: 14_000,
32
+ items: [
33
+ { ref: 'file:src/lib/cli-preferences.js', kind: 'file', priority: 100, content: 'CLI preference schema and persistence.'.repeat(90) },
34
+ { ref: 'file:src/commands/shell.js', kind: 'file', priority: 98, content: 'Slash command parsing, runtime cwd, model and context controls.'.repeat(120) },
35
+ { ref: 'file:apps/cad-creator/frontend/app.tsx', kind: 'file', priority: 5, content: 'CAD UI.'.repeat(300) },
36
+ { ref: 'file:docs/identity.md', kind: 'file', priority: 4, content: 'Identity docs.'.repeat(240) },
37
+ ],
38
+ },
39
+ 'config-without-runtime-pollution': {
40
+ task: 'Update portable project defaults while keeping account, run, connection, and execution state out of committed config.',
41
+ required: ['file:.agentsam/config.json', 'file:src/lib/cli-preferences.js'],
42
+ syntheticBaseTokens: 10_000,
43
+ items: [
44
+ { ref: 'file:.agentsam/config.json', kind: 'file', priority: 100, content: 'Portable repository identity and defaults only.'.repeat(90) },
45
+ { ref: 'file:src/lib/cli-preferences.js', kind: 'file', priority: 90, content: 'Gitignored per-machine model, runtime, terminal, and trust preferences.'.repeat(85) },
46
+ { ref: 'memory:run-history', kind: 'memory', priority: 3, content: 'Large hosted run history must not be copied into project config.'.repeat(500), consumed: true },
47
+ ],
48
+ },
49
+ 'trace-failing-test': {
50
+ task: 'Trace a failing shell regression test to the exact implementation and fix only the implicated source.',
51
+ required: ['file:test/shell.test.mjs', 'file:src/commands/shell.js'],
52
+ syntheticBaseTokens: 22_000,
53
+ items: [
54
+ { ref: 'file:test/shell.test.mjs', kind: 'file', priority: 100, content: 'Regression assertions for slash dispatch and runtime cwd.'.repeat(110) },
55
+ { ref: 'file:src/commands/shell.js', kind: 'file', priority: 96, content: 'Command implementation under test.'.repeat(140) },
56
+ { ref: 'file:test/cad.test.mjs', kind: 'file', priority: 2, content: 'Unrelated CAD tests.'.repeat(240) },
57
+ ],
58
+ },
59
+ 'contradictory-paths': {
60
+ task: 'Detect two contradictory model-selection paths and choose one canonical runtime preference path.',
61
+ required: ['file:src/commands/models.js', 'file:src/commands/preferences.js', 'file:src/lib/cli-preferences.js'],
62
+ syntheticBaseTokens: 36_000,
63
+ items: [
64
+ { ref: 'file:src/commands/models.js', kind: 'file', priority: 100, content: 'Provider model discovery and availability proof.'.repeat(105) },
65
+ { ref: 'file:src/commands/preferences.js', kind: 'file', priority: 98, content: 'Interactive model, reasoning, and processing selection.'.repeat(100) },
66
+ { ref: 'file:src/lib/cli-preferences.js', kind: 'file', priority: 96, content: 'Canonical local preference persistence.'.repeat(90) },
67
+ { ref: 'file:legacy/model-picker.js', kind: 'file', priority: 15, content: 'Legacy duplicate picker path that should not become a second authority.'.repeat(200), consumed: true },
68
+ ],
69
+ },
70
+ 'continuation-after-compaction': {
71
+ task: 'Continue a long tool-heavy run by compacting consumed evidence and rehydrating the exact result needed for the next step.',
72
+ required: ['tool:call_previous', 'file:src/context/compact.js'],
73
+ syntheticBaseTokens: 174_000,
74
+ items: [
75
+ { ref: 'tool:call_previous', kind: 'tool_result', priority: 100, content: 'Critical prior tool evidence with stable identity and the exact invariant required later. '.repeat(900), consumed: true },
76
+ { ref: 'file:src/context/compact.js', kind: 'file', priority: 95, content: 'Compaction preserves ref hash and source size while shrinking active context.'.repeat(100) },
77
+ { ref: 'file:unrelated/generated.log', kind: 'file', priority: 1, content: 'Noisy old generated log.'.repeat(500), consumed: true },
78
+ ],
79
+ },
80
+ });
81
+
82
+ function budgetFor(record) {
83
+ const p = record.context_policy;
84
+ return createContextBudget({ windowTokens: record.context_window, targetInputTokens: p.target_input_tokens, compactAtTokens: p.compact_at_tokens, interveneAtTokens: p.intervene_at_tokens, maxNormalInputTokens: p.max_normal_input_tokens, pricingThresholdTokens: p.pricing_threshold_tokens, safetyMarginTokens: p.safety_margin_tokens });
85
+ }
86
+
87
+ function fixture(name) {
88
+ const value = FIXTURES[name];
89
+ if (!value) throw new Error(`unknown context eval fixture: ${name}`);
90
+ return value;
91
+ }
92
+
93
+ async function runStrategy(fx, strategy, record) {
94
+ const budget = budgetFor(record);
95
+ let items = fx.items.map(item => ({ ...item }));
96
+ let compactedChars = 0;
97
+ const sources = new Map(items.map(item => [item.ref, item]));
98
+ const rehydratedRefs = [];
99
+
100
+ if (strategy === 'compact') {
101
+ items = items.map(item => {
102
+ if (!item.consumed) return item;
103
+ const compacted = compactContextItem(item, { maxChars: 4_000 });
104
+ compactedChars += Math.max(0, item.content.length - compacted.content.length);
105
+ return compacted;
106
+ });
107
+ }
108
+
109
+ const tools = strategy === 'bounded' ? { cards: [], receipt: { returned_items: 0, chars: 0 } } : searchToolCards(BASE_TOOLS, fx.task, { maxItems: 8 });
110
+ const hydrated = strategy === 'bounded' ? { tools: [], receipt: { hydrated_tools: 0, schema_chars: 0 } } : hydrateToolSchemas(BASE_TOOLS, tools.cards.map(card => card.tool), { maxTools: 8, maxChars: 40_000 });
111
+ let pack = resolveContext({ objective: fx.task, budget, toolSchemaChars: hydrated.receipt.schema_chars, items });
112
+ const selectedRefs = new Set(pack.items.map(item => item.ref));
113
+
114
+ if (strategy === 'compact') {
115
+ for (const ref of fx.required) {
116
+ const selected = pack.items.find(item => item.ref === ref);
117
+ if (!selected?.compacted) continue;
118
+ const source = sources.get(ref);
119
+ const rehydrated = await rehydrateContextRef(ref, async () => source, { kind: source.kind, maxChars: budget.maxFileCharsPerRead });
120
+ const remaining = pack.items.filter(item => item.ref !== ref);
121
+ remaining.push({ ...rehydrated, priority: source.priority });
122
+ pack = resolveContext({ objective: fx.task, budget, toolSchemaChars: hydrated.receipt.schema_chars, items: remaining });
123
+ selectedRefs.add(ref);
124
+ rehydratedRefs.push(ref);
125
+ }
126
+ }
127
+
128
+ const missingRequired = fx.required.filter(ref => !selectedRefs.has(ref));
129
+ const evidenceTokens = pack.receipt.estimated_input_tokens;
130
+ const activeTokens = fx.syntheticBaseTokens + evidenceTokens;
131
+ const pressure = assessContextUsage(activeTokens, budget);
132
+ const inputCost = calculateModelCost(record, { input_tokens: activeTokens, output_tokens: 0 }, { serviceTier: 'default' });
133
+ return Object.freeze({
134
+ strategy,
135
+ result: missingRequired.length ? 'FAIL' : 'PASS',
136
+ required_evidence: fx.required.length,
137
+ required_found: fx.required.length - missingRequired.length,
138
+ missing_required: Object.freeze(missingRequired),
139
+ sources_considered: pack.receipt.sources_considered,
140
+ sources_selected: pack.receipt.sources_included,
141
+ sources_deferred: pack.receipt.sources_deferred,
142
+ tool_cards: tools.receipt.returned_items,
143
+ tool_card_chars: tools.receipt.chars,
144
+ hydrated_tools: hydrated.receipt.hydrated_tools,
145
+ tool_schema_chars: hydrated.receipt.schema_chars,
146
+ compacted_chars: compactedChars,
147
+ rehydrated_refs: Object.freeze(rehydratedRefs),
148
+ active_context_tokens: activeTokens,
149
+ evidence_tokens: evidenceTokens,
150
+ window_tokens: budget.windowTokens,
151
+ pricing_threshold_tokens: budget.pricingThresholdTokens,
152
+ tokens_until_pricing_threshold: pressure.tokensUntilPricingThreshold,
153
+ pressure: pressure.stage,
154
+ should_compact: pressure.shouldCompact,
155
+ estimated_input_cost_usd: inputCost.total_usd,
156
+ estimate_kind: 'local',
157
+ });
158
+ }
159
+
160
+ function rank(results) {
161
+ return [...results].sort((a, b) => {
162
+ if (a.result !== b.result) return a.result === 'PASS' ? -1 : 1;
163
+ if (a.required_found !== b.required_found) return b.required_found - a.required_found;
164
+ if (a.active_context_tokens !== b.active_context_tokens) return a.active_context_tokens - b.active_context_tokens;
165
+ return a.tool_schema_chars - b.tool_schema_chars;
166
+ });
167
+ }
168
+
169
+ export function listContextEvalFixtures() { return Object.keys(FIXTURES); }
170
+
171
+ export async function evaluateContextFixture(options = {}) {
172
+ const name = options.fixture || 'exact-symbol-callers';
173
+ const fx = fixture(name);
174
+ const record = getModelRecord(options.model || 'gpt-6-astra');
175
+ if (!record) throw new Error(`unknown model: ${options.model}`);
176
+ const strategies = options.strategy && options.strategy !== 'all' ? [options.strategy] : STRATEGIES;
177
+ for (const strategy of strategies) if (!STRATEGIES.includes(strategy)) throw new Error(`unknown context strategy: ${strategy}`);
178
+ const results = [];
179
+ for (const strategy of strategies) results.push(await runStrategy(fx, strategy, record));
180
+ const ranked = rank(results);
181
+ return Object.freeze({
182
+ schema_version: 1,
183
+ fixture: name,
184
+ task: fx.task,
185
+ model: record.provider_model_id,
186
+ live_provider_used: false,
187
+ strategies: Object.freeze(results),
188
+ winner: ranked[0]?.strategy || null,
189
+ scoring: Object.freeze(['correctness', 'required_evidence', 'context_efficiency', 'tool_efficiency']),
190
+ });
191
+ }
@@ -0,0 +1 @@
1
+ export { listContextEvalFixtures, evaluateContextFixture } from './context.js';
package/src/index.js CHANGED
@@ -4,7 +4,7 @@ import pkg from '../package.json' with { type: 'json' };
4
4
 
5
5
  export { AgentSam } from './AgentSam.js';
6
6
  export { routeIntent } from './lib/router.js';
7
- export { searchToolCards, toToolCard } from './tools/index.js';
7
+ export { searchToolCards, toToolCard, hydrateToolSchemas } from './tools/index.js';
8
8
  export { getToolCatalog } from './lib/tools.js';
9
9
  export { scaffoldProject } from './lib/scaffold.js';
10
10
  export {
@@ -44,17 +44,71 @@ export {
44
44
  DEFAULT_CONTEXT_RATIOS,
45
45
  DEFAULT_RESULT_POLICY,
46
46
  createContextBudget,
47
+ assessContextUsage,
47
48
  normalizeResultPolicy,
48
49
  resolveContext,
49
50
  resolveProjectContext,
50
51
  compactConsumedToolResult,
52
+ rehydrateContextRef,
51
53
  loadProjectRules,
54
+ compileAgentInstructions,
55
+ AGENT_INSTRUCTION_PRECEDENCE,
52
56
  } from './context/index.js';
53
57
  export {
54
58
  assertRepositoryKnowledgeProvider,
55
59
  createRepositoryKnowledgeClient,
56
60
  describeRepositoryKnowledgeProvider,
57
61
  } from './indexing/index.js';
62
+ export {
63
+ MODEL_CATALOG_SCHEMA,
64
+ MODEL_CATALOG,
65
+ listModelCatalog,
66
+ getModelRecord,
67
+ calculateModelCost,
68
+ } from './models/index.js';
69
+ export {
70
+ AGENT_EVENT_TYPES,
71
+ RUNTIME_RECEIPT_SCHEMA_VERSION,
72
+ createAgentEvent,
73
+ createUsageSnapshot,
74
+ createRunReceipt,
75
+ createUsageReceipt,
76
+ createApprovalReceipt,
77
+ createTerminalJobReceipt,
78
+ } from './telemetry/index.js';
79
+ export {
80
+ createOpenAIResponsesAdapter,
81
+ extractOpenAIOutputText,
82
+ extractOpenAIFunctionCalls,
83
+ } from './providers/index.js';
84
+ export {
85
+ AgentSamDiagnosticError,
86
+ classifyOpenAIError,
87
+ createOpenAIHttpError,
88
+ createProcessDiagnosticError,
89
+ diagnosticFromError,
90
+ redactDiagnosticValue,
91
+ renderDiagnosticError,
92
+ } from './errors/index.js';
93
+ export {
94
+ WRANGLER_NATIVE_COMMANDS,
95
+ WRANGLER_OPERATION_FAMILIES,
96
+ buildWranglerInvocation,
97
+ listWranglerNativeCommands,
98
+ parseWranglerErrorEvidence,
99
+ runWranglerNative,
100
+ summarizeCloudflareCpuProfile,
101
+ summarizeCloudflareCpuProfileFile,
102
+ buildCloudflareCpuAuditPacket,
103
+ runCloudflareCpuAudit,
104
+ } from './cloudflare/index.js';
105
+ export {
106
+ createCapabilityAdapter,
107
+ buildAgentToolSurface,
108
+ capabilityFunctionName,
109
+ runResponsesAgent,
110
+ } from './agent/index.js';
111
+ export { listContextEvalFixtures, evaluateContextFixture } from './eval/index.js';
58
112
 
59
113
  export {
60
114
  createIdentityClient,
@@ -0,0 +1,98 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { resolveSdkKey } from '../../packages/identity/src/contracts/auth-config.js';
5
+
6
+ export const ACCOUNT_SESSION_SCHEMA = 'agentsam-account-session-v1';
7
+
8
+ function clean(value) { return value == null ? '' : String(value).trim(); }
9
+ function homeDirectory(options = {}) {
10
+ return path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir());
11
+ }
12
+
13
+ export function accountSessionPath(options = {}) {
14
+ return path.join(homeDirectory(options), '.agentsam', 'auth', 'session.json');
15
+ }
16
+
17
+ export function readAccountSession(options = {}) {
18
+ const filename = accountSessionPath(options);
19
+ if (!fs.existsSync(filename)) return null;
20
+ try {
21
+ const stat = fs.statSync(filename);
22
+ if (!stat.isFile()) return null;
23
+ if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) return null;
24
+ const parsed = JSON.parse(fs.readFileSync(filename, 'utf8'));
25
+ if (parsed?.schema_version !== ACCOUNT_SESSION_SCHEMA) return null;
26
+ const token = clean(parsed.sdk_key);
27
+ if (!token.startsWith('sdk_')) return null;
28
+ return {
29
+ schema_version: ACCOUNT_SESSION_SCHEMA,
30
+ sdk_key: token,
31
+ user_id: clean(parsed.user_id) || null,
32
+ account_id: clean(parsed.account_id) || null,
33
+ email: clean(parsed.email) || null,
34
+ created_at: clean(parsed.created_at) || null,
35
+ updated_at: clean(parsed.updated_at) || null,
36
+ };
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ export function saveAccountSession(session = {}, options = {}) {
43
+ const token = clean(session.sdk_key || session.access_token);
44
+ if (!token.startsWith('sdk_')) throw new Error('account_session_sdk_key_required');
45
+ const filename = accountSessionPath(options);
46
+ const dir = path.dirname(filename);
47
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
48
+ if (process.platform !== 'win32') {
49
+ try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
50
+ }
51
+ const previous = readAccountSession(options);
52
+ const now = new Date().toISOString();
53
+ const value = {
54
+ schema_version: ACCOUNT_SESSION_SCHEMA,
55
+ sdk_key: token,
56
+ user_id: clean(session.user_id) || previous?.user_id || null,
57
+ account_id: clean(session.account_id) || previous?.account_id || null,
58
+ email: clean(session.email) || previous?.email || null,
59
+ created_at: previous?.created_at || now,
60
+ updated_at: now,
61
+ };
62
+ const temp = `${filename}.${process.pid}.tmp`;
63
+ fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
64
+ if (process.platform !== 'win32') {
65
+ try { fs.chmodSync(temp, 0o600); } catch { /* best effort */ }
66
+ }
67
+ fs.renameSync(temp, filename);
68
+ return { ...value };
69
+ }
70
+
71
+ export function clearAccountSession(options = {}) {
72
+ const filename = accountSessionPath(options);
73
+ if (!fs.existsSync(filename)) return false;
74
+ fs.rmSync(filename, { force: true });
75
+ return true;
76
+ }
77
+
78
+ export function resolveAccountSdkKey(options = {}) {
79
+ const env = options.env || process.env;
80
+ const explicit = clean(options.explicit);
81
+ const fromEnv = resolveSdkKey(env, explicit);
82
+ if (fromEnv) return { value: fromEnv, source: explicit ? 'explicit' : 'environment' };
83
+ const session = readAccountSession({ ...options, env });
84
+ return session?.sdk_key
85
+ ? { value: session.sdk_key, source: 'agentsam_account_session', session }
86
+ : { value: '', source: null, session: null };
87
+ }
88
+
89
+ export function describeAccountSession(options = {}) {
90
+ const resolved = resolveAccountSdkKey(options);
91
+ return {
92
+ configured: Boolean(resolved.value),
93
+ source: resolved.source,
94
+ user_id: resolved.session?.user_id || null,
95
+ account_id: resolved.session?.account_id || null,
96
+ email: resolved.session?.email || null,
97
+ };
98
+ }
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { findProjectRules } from './project-rules.js';
5
+
6
+ export const AGENT_RUNTIME_FILENAME = 'AGENTSAM.md';
7
+ export const AGENT_INSTRUCTION_PRECEDENCE = Object.freeze(['AGENTSAM.md', '.agentsamrules']);
8
+
9
+ function sha256(value) {
10
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
11
+ }
12
+
13
+ function findUp(startDir, filename) {
14
+ let dir = path.resolve(startDir);
15
+ for (let i = 0; i < 16; i += 1) {
16
+ const candidate = path.join(dir, filename);
17
+ if (fs.existsSync(candidate)) return candidate;
18
+ const boundary = fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, '.agentsam', 'config.json'));
19
+ if (boundary) break;
20
+ const parent = path.dirname(dir);
21
+ if (parent === dir) break;
22
+ dir = parent;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export function findAgentRuntimeContract(startDir = process.cwd()) {
28
+ return findUp(startDir, AGENT_RUNTIME_FILENAME);
29
+ }
30
+
31
+ export function compileAgentInstructions(startDir = process.cwd(), options = {}) {
32
+ const maxChars = Number(options.maxChars ?? 24_000);
33
+ if (!Number.isInteger(maxChars) || maxChars < 1) throw new RangeError('agent instructions maxChars must be a positive integer');
34
+
35
+ const runtimePath = options.runtimeFilename ? path.resolve(options.runtimeFilename) : findAgentRuntimeContract(startDir);
36
+ const projectPath = options.projectFilename ? path.resolve(options.projectFilename) : findProjectRules(startDir);
37
+ const descriptors = [
38
+ { id: 'runtime', filename: AGENT_RUNTIME_FILENAME, path: runtimePath },
39
+ { id: 'project', filename: '.agentsamrules', path: projectPath },
40
+ ];
41
+ const sections = [];
42
+ const sources = [];
43
+ let sourceChars = 0;
44
+
45
+ for (const descriptor of descriptors) {
46
+ if (!descriptor.path || !fs.existsSync(descriptor.path)) continue;
47
+ const source = fs.readFileSync(descriptor.path, 'utf8');
48
+ sourceChars += source.length;
49
+ sources.push(Object.freeze({
50
+ id: descriptor.id,
51
+ filename: descriptor.filename,
52
+ path: descriptor.path,
53
+ chars: source.length,
54
+ hash: sha256(source),
55
+ }));
56
+ sections.push(`<!-- agentsam:${descriptor.id}:${descriptor.filename} -->\n${source.trim()}\n`);
57
+ }
58
+
59
+ const combined = sections.join('\n');
60
+ const truncated = combined.length > maxChars;
61
+ const content = truncated ? `${combined.slice(0, Math.max(0, maxChars - 1))}…` : combined;
62
+ return Object.freeze({
63
+ found: sources.length > 0,
64
+ path: projectPath || runtimePath || null,
65
+ content,
66
+ chars: content.length,
67
+ source_chars: sourceChars,
68
+ truncated,
69
+ hash: combined ? sha256(combined) : null,
70
+ precedence: AGENT_INSTRUCTION_PRECEDENCE,
71
+ sources: Object.freeze(sources),
72
+ });
73
+ }
package/src/lib/auth.js CHANGED
@@ -5,6 +5,7 @@ import http from 'node:http';
5
5
  import { randomBytes } from 'node:crypto';
6
6
  import { postJson } from './core-client.js';
7
7
  import { promptToOpenUrl } from './open-url.js';
8
+ import { saveAccountSession } from './account-session.js';
8
9
 
9
10
  function randomState() {
10
11
  return randomBytes(16).toString('hex');
@@ -63,5 +64,8 @@ export async function authenticateViaBrowser() {
63
64
 
64
65
  const code = await codePromise;
65
66
  const session = await postJson('/api/sdk/auth/exchange', { code, state });
67
+ if (String(session?.access_token || '').trim().startsWith('sdk_')) {
68
+ saveAccountSession(session);
69
+ }
66
70
  return session;
67
71
  }
@@ -3,14 +3,12 @@ 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-v1';
6
+ export const CLI_PREFERENCES_SCHEMA = 'agentsam-cli-preferences-v2';
7
+ export const LEGACY_CLI_PREFERENCES_SCHEMA = 'agentsam-cli-preferences-v1';
7
8
 
8
9
  function readJson(filename) {
9
- try {
10
- return JSON.parse(fs.readFileSync(filename, 'utf8'));
11
- } catch {
12
- return null;
13
- }
10
+ try { return JSON.parse(fs.readFileSync(filename, 'utf8')); }
11
+ catch { return null; }
14
12
  }
15
13
 
16
14
  function gitValue(cwd, args) {
@@ -22,14 +20,9 @@ export function findCliProjectRoot(startDir = process.cwd()) {
22
20
  const cwd = path.resolve(startDir);
23
21
  const gitRoot = gitValue(cwd, ['rev-parse', '--show-toplevel']);
24
22
  if (gitRoot) return path.resolve(gitRoot);
25
-
26
23
  let dir = cwd;
27
24
  for (let i = 0; i < 16; i += 1) {
28
- if (
29
- fs.existsSync(path.join(dir, '.agentsam', 'cli.json')) ||
30
- fs.existsSync(path.join(dir, '.agentsam', 'config.json')) ||
31
- fs.existsSync(path.join(dir, 'package.json'))
32
- ) return dir;
25
+ if (fs.existsSync(path.join(dir, '.agentsam', 'cli.json')) || fs.existsSync(path.join(dir, '.agentsam', 'config.json')) || fs.existsSync(path.join(dir, 'package.json'))) return dir;
33
26
  const parent = path.dirname(dir);
34
27
  if (parent === dir) break;
35
28
  dir = parent;
@@ -48,27 +41,38 @@ export function detectCliProject(startDir = process.cwd()) {
48
41
  return { root, project, branch, remote, website, configured: Boolean(getProjectName(config)) };
49
42
  }
50
43
 
51
- export function cliPreferencesPath(root) {
52
- return path.join(path.resolve(root), '.agentsam', 'cli.json');
44
+ export function cliPreferencesPath(root) { return path.join(path.resolve(root), '.agentsam', 'cli.json'); }
45
+
46
+ function normalizePreferences(value = {}) {
47
+ return {
48
+ schemaVersion: CLI_PREFERENCES_SCHEMA,
49
+ trustedDirectory: value.trustedDirectory === true,
50
+ runtime: value.runtime || 'local',
51
+ terminal: value.terminal || '',
52
+ modelPreference: value.modelPreference || 'auto',
53
+ reasoningEffort: value.reasoningEffort || 'auto',
54
+ serviceTier: value.serviceTier || 'default',
55
+ modelAuthority: 'preference-only',
56
+ updatedAt: value.updatedAt || null,
57
+ };
53
58
  }
54
59
 
55
60
  export function readCliPreferences(root) {
56
61
  const value = readJson(cliPreferencesPath(root));
57
- if (!value || value.schemaVersion !== CLI_PREFERENCES_SCHEMA) return null;
58
- return value;
62
+ if (!value) return null;
63
+ if (value.schemaVersion !== CLI_PREFERENCES_SCHEMA && value.schemaVersion !== LEGACY_CLI_PREFERENCES_SCHEMA) return null;
64
+ return normalizePreferences(value);
59
65
  }
60
66
 
61
67
  export function writeCliPreferences(root, value = {}) {
62
68
  const filename = cliPreferencesPath(root);
63
69
  fs.mkdirSync(path.dirname(filename), { recursive: true });
64
- const next = {
65
- schemaVersion: CLI_PREFERENCES_SCHEMA,
66
- runtime: value.runtime || 'local',
67
- terminal: value.terminal || '',
68
- modelPreference: value.modelPreference || 'auto',
69
- modelAuthority: 'preference-only',
70
- updatedAt: new Date().toISOString(),
71
- };
70
+ const previous = readCliPreferences(root) || {};
71
+ const next = normalizePreferences({ ...previous, ...value, updatedAt: new Date().toISOString() });
72
72
  fs.writeFileSync(filename, `${JSON.stringify(next, null, 2)}\n`);
73
73
  return next;
74
74
  }
75
+
76
+ export function updateCliPreferences(root, patch = {}) {
77
+ return writeCliPreferences(root, { ...(readCliPreferences(root) || {}), ...patch });
78
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Production Wrangler deploys are allowed only from a clean main that matches origin/main.
3
+ * Dry-run / plan may run from a feature branch. Never from /private/tmp.
4
+ */
5
+ import path from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
+
8
+ const BLOCKED_BRANCH_PREFIXES = ['feat/', 'fix/', 'chore/', 'release/'];
9
+
10
+ function git(cwd, args) {
11
+ const r = spawnSync('git', args, { cwd, encoding: 'utf8' });
12
+ return {
13
+ status: r.status,
14
+ stdout: String(r.stdout || '').trim(),
15
+ stderr: String(r.stderr || '').trim(),
16
+ };
17
+ }
18
+
19
+ export function inspectDeployGit(cwd = process.cwd()) {
20
+ const root = git(cwd, ['rev-parse', '--show-toplevel']);
21
+ const branch = git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
22
+ const head = git(cwd, ['rev-parse', 'HEAD']);
23
+ const origin = git(cwd, ['rev-parse', 'origin/main']);
24
+ const porcelain = git(cwd, ['status', '--porcelain']);
25
+ const detached = branch.stdout === 'HEAD';
26
+ const repoRoot = root.stdout || path.resolve(cwd);
27
+ return {
28
+ ok: root.status === 0,
29
+ repoRoot,
30
+ branch: branch.stdout,
31
+ detached,
32
+ head: head.stdout,
33
+ originMain: origin.stdout,
34
+ dirty: Boolean(porcelain.stdout),
35
+ tmpCheckout: repoRoot.includes('/private/tmp/') || repoRoot.includes('/tmp/'),
36
+ };
37
+ }
38
+
39
+ export function productionDeployBlockedReason(info) {
40
+ if (!info?.ok) return 'not a git checkout';
41
+ if (info.tmpCheckout) return 'refusing production deploy from a temporary checkout';
42
+ if (info.detached) return 'refusing production deploy from detached HEAD';
43
+ if (info.branch !== 'main') {
44
+ return `refusing production deploy from ${info.branch}; origin/main only`;
45
+ }
46
+ for (const prefix of BLOCKED_BRANCH_PREFIXES) {
47
+ if (info.branch.startsWith(prefix)) {
48
+ return `refusing production deploy from ${info.branch}`;
49
+ }
50
+ }
51
+ if (!info.originMain) return 'origin/main is missing; fetch before deploying';
52
+ if (info.head !== info.originMain) {
53
+ return `HEAD (${info.head.slice(0, 12)}) != origin/main (${info.originMain.slice(0, 12)})`;
54
+ }
55
+ if (info.dirty) return 'working tree is not clean';
56
+ return null;
57
+ }
58
+
59
+ export function assertProductionDeployAllowed(cwd = process.cwd()) {
60
+ const info = inspectDeployGit(cwd);
61
+ const reason = productionDeployBlockedReason(info);
62
+ if (reason) {
63
+ const err = new Error(reason);
64
+ err.code = 'production_deploy_refused';
65
+ err.git = info;
66
+ throw err;
67
+ }
68
+ return info;
69
+ }