@inneranimalmedia/agentsam-sdk 2.5.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.
Files changed (211) 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/PLATFORM_RUNTIME_EVENTS.md +48 -0
  7. package/docs/RELEASES.md +16 -7
  8. package/docs/SOURCE_ARCHITECTURE.md +58 -0
  9. package/docs/TEST_TIERS.md +26 -0
  10. package/migrations/runtime/0001_cli_runtime.sql +298 -0
  11. package/package.json +45 -12
  12. package/packages/agentsam-repository/README.md +15 -0
  13. package/packages/agentsam-repository/package.json +25 -0
  14. package/packages/agentsam-repository/src/contracts.js +113 -0
  15. package/packages/agentsam-repository/src/index.js +3 -0
  16. package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
  17. package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
  18. package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
  19. package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
  20. package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
  21. package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
  22. package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
  23. package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
  24. package/packages/connectors/cloudflare/package.json +10 -0
  25. package/packages/connectors/cloudflare/src/index.js +127 -0
  26. package/packages/connectors/cloudflare/src/owner.js +76 -0
  27. package/packages/connectors/cloudflare/src/routes.js +223 -0
  28. package/packages/connectors/cloudflare/src/vault.js +80 -0
  29. package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
  30. package/packages/identity/package.json +2 -2
  31. package/packages/identity/src/contracts/auth-config.js +18 -7
  32. package/packages/identity/tests/auth-config.test.mjs +9 -5
  33. package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
  34. package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
  35. package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
  36. package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
  37. package/protocol/README.md +1 -0
  38. package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
  39. package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
  40. package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
  41. package/protocol/capabilities/manifest.json +47 -0
  42. package/protocol/context/context-budget.schema.json +10 -15
  43. package/protocol/context/context-item.schema.json +4 -5
  44. package/protocol/context/resolved-context-pack.schema.json +19 -14
  45. package/protocol/models/README.md +373 -0
  46. package/protocol/models/model-inventory-v2.schema.json +212 -0
  47. package/protocol/repository/repository-contract.schema.json +24 -0
  48. package/protocol/repository/repository-dependency.schema.json +24 -0
  49. package/protocol/repository/repository-identity.schema.json +17 -0
  50. package/protocol/rpc/v1/common.proto +16 -0
  51. package/protocol/rpc/v1/errors.proto +35 -0
  52. package/protocol/rpc/v1/knowledge.proto +77 -0
  53. package/services/knowledge/package-lock.json +333 -0
  54. package/services/knowledge/package.json +5 -1
  55. package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
  56. package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
  57. package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
  58. package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
  59. package/skills/catalog.json +18 -0
  60. package/src/agent/capability-adapter.js +25 -13
  61. package/src/agent/index.js +1 -0
  62. package/src/agent/responses-runner.js +353 -0
  63. package/src/capabilities/repository-snapshot.js +3 -3
  64. package/src/cli.js +118 -31
  65. package/src/cloudflare/cpu-profile.js +115 -0
  66. package/src/cloudflare/index.js +14 -0
  67. package/src/cloudflare/wrangler.js +132 -0
  68. package/src/commands/account-auth.js +47 -0
  69. package/src/commands/cloudflare.js +58 -0
  70. package/src/commands/connections.js +93 -0
  71. package/src/commands/context-economics.js +129 -0
  72. package/src/commands/context.js +1 -1
  73. package/src/commands/db.js +20 -3
  74. package/src/commands/deploy.js +39 -3
  75. package/src/commands/env.js +90 -0
  76. package/src/commands/eval.js +63 -0
  77. package/src/commands/interactive.js +2 -5
  78. package/src/commands/knowledge.js +12 -4
  79. package/src/commands/merkle-persist.js +30 -11
  80. package/src/commands/merkle.js +1 -1
  81. package/src/commands/models.js +149 -46
  82. package/src/commands/ollama.js +26 -0
  83. package/src/commands/preferences.js +130 -61
  84. package/src/commands/resume.js +67 -0
  85. package/src/commands/security.js +5 -3
  86. package/src/commands/shell.js +568 -119
  87. package/src/commands/tunnel.js +2 -2
  88. package/src/commands/whoami.js +86 -0
  89. package/src/context/budget.js +68 -6
  90. package/src/context/index.js +3 -1
  91. package/src/context/rehydrate.js +35 -0
  92. package/src/context/resolve.js +44 -12
  93. package/src/errors/contract.js +236 -0
  94. package/src/errors/diagnostic.js +160 -0
  95. package/src/errors/index.js +23 -0
  96. package/src/eval/context.js +191 -0
  97. package/src/eval/index.js +1 -0
  98. package/src/index.js +68 -2
  99. package/src/knowledge/service/auth.js +13 -0
  100. package/src/knowledge/service/grpc-client.js +115 -0
  101. package/src/knowledge/service/grpc-codec.js +237 -0
  102. package/src/knowledge/service/grpc-server.js +83 -0
  103. package/src/knowledge/service/job-engine.js +248 -0
  104. package/src/knowledge/service/server.js +87 -135
  105. package/src/knowledge/source.js +1 -1
  106. package/src/lib/account-session.js +98 -0
  107. package/src/lib/agent-instructions.js +73 -0
  108. package/src/lib/auth.js +4 -0
  109. package/src/lib/cli-preferences.js +55 -24
  110. package/src/lib/deploy/git-guard.js +69 -0
  111. package/src/lib/deploy/health.js +57 -0
  112. package/src/lib/deploy/local-studio.js +283 -0
  113. package/src/lib/deploy/secret-scan.js +65 -0
  114. package/src/lib/deploy-receipt/index.js +2 -2
  115. package/src/lib/detect-context.js +2 -2
  116. package/src/lib/execution-approvals.js +59 -0
  117. package/src/lib/knowledge-docker.js +6 -3
  118. package/src/lib/local-sessions.js +148 -0
  119. package/src/lib/local-status.js +1 -1
  120. package/src/lib/project-config.js +1 -1
  121. package/src/lib/provider-credentials.js +183 -0
  122. package/src/lib/scaffold/templates/worker-api/index.js +101 -20
  123. package/src/lib/scaffold/wizards/worker-api.js +27 -11
  124. package/src/lib/slash-commands.js +23 -16
  125. package/src/local/migrations.js +93 -0
  126. package/src/local/runtime-store.js +141 -0
  127. package/src/local/sqlite.js +2 -0
  128. package/src/local-pty/server.js +113 -51
  129. package/src/models/catalog.js +135 -0
  130. package/src/models/discovery.js +292 -0
  131. package/src/models/index.js +7 -0
  132. package/src/providers/anthropic-messages.js +192 -0
  133. package/src/providers/cloudflare-chat.js +183 -0
  134. package/src/providers/factory.js +69 -0
  135. package/src/providers/gemini-generate-content.js +208 -0
  136. package/src/providers/index.js +10 -0
  137. package/src/providers/ollama-chat.js +148 -0
  138. package/src/providers/openai-responses.js +426 -0
  139. package/src/repository/index.js +14 -2
  140. package/src/rpc/generated/common_grpc_pb.js +1 -0
  141. package/src/rpc/generated/common_pb.js +536 -0
  142. package/src/rpc/generated/errors_grpc_pb.js +1 -0
  143. package/src/rpc/generated/errors_pb.js +482 -0
  144. package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
  145. package/src/rpc/generated/knowledge_pb.js +2168 -0
  146. package/src/rpc/generated/package.json +3 -0
  147. package/src/security/process.js +35 -9
  148. package/src/security/trust-boundary.js +2 -2
  149. package/src/telemetry/contracts.js +203 -0
  150. package/src/telemetry/events.js +51 -0
  151. package/src/telemetry/index.js +8 -0
  152. package/src/tools/hydrate.js +35 -0
  153. package/src/tools/index.js +1 -0
  154. package/src/ui/boot.js +15 -17
  155. package/src/ui/cli/activity.js +76 -0
  156. package/src/ui/cli/compaction.js +15 -0
  157. package/src/ui/cli/footer.js +39 -0
  158. package/src/ui/cli/help.js +192 -0
  159. package/src/ui/cli/plan.js +20 -0
  160. package/src/ui/cli/runtime-events.js +110 -0
  161. package/src/ui/cli/waiting.js +16 -0
  162. package/src/ui/merkle/render.js +1 -1
  163. package/test/account-session.test.mjs +36 -0
  164. package/test/cli/preferences-runtime.test.mjs +11 -0
  165. package/test/cli/runtime-ui.test.mjs +74 -0
  166. package/test/cli-preferences.test.mjs +26 -5
  167. package/test/cloudflare-connector.test.mjs +96 -0
  168. package/test/cloudflare-runtime.test.mjs +75 -0
  169. package/test/context.test.mjs +61 -12
  170. package/test/deploy-health-scan.test.mjs +67 -0
  171. package/test/error-diagnostics.test.mjs +115 -0
  172. package/test/eval-context.test.mjs +37 -0
  173. package/test/execution-approvals.test.mjs +27 -0
  174. package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
  175. package/test/integration/cli-help.test.mjs +37 -0
  176. package/test/integration/knowledge-rpc.test.mjs +112 -0
  177. package/test/integration/merkle-cli.test.mjs +61 -0
  178. package/test/integration/merkle-persistence-identity.test.mjs +48 -0
  179. package/test/integration/provider-env-cli.test.mjs +49 -0
  180. package/test/integration/provider-factory.test.mjs +197 -0
  181. package/test/integration/repository-company-graph.test.mjs +90 -0
  182. package/test/integration/runtime-migrations.test.mjs +82 -0
  183. package/test/knowledge-service.test.mjs +5 -0
  184. package/test/knowledge.test.mjs +16 -0
  185. package/test/live/terminal-transport.live.test.mjs +24 -0
  186. package/test/local-sessions.test.mjs +48 -0
  187. package/test/local-studio-deploy.test.mjs +83 -0
  188. package/test/model-catalog.test.mjs +43 -0
  189. package/test/models.test.mjs +127 -16
  190. package/test/npm10-lock.test.mjs +29 -0
  191. package/test/ollama.test.mjs +21 -0
  192. package/test/openai-responses.test.mjs +95 -0
  193. package/test/portable-context.test.mjs +1 -1
  194. package/test/provider-credentials.test.mjs +96 -0
  195. package/test/rehydrate.test.mjs +25 -0
  196. package/test/release-hygiene.test.mjs +13 -5
  197. package/test/responses-runner.test.mjs +150 -0
  198. package/test/shell.test.mjs +92 -23
  199. package/test/smoke.mjs +4 -1
  200. package/test/telemetry.test.mjs +79 -0
  201. package/test/terminal/local-pty.mock.test.mjs +151 -0
  202. package/test/tools-search.test.mjs +14 -1
  203. package/test/whoami-resume.test.mjs +56 -0
  204. /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
  205. /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
  206. /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
  207. /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
  208. /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
  209. /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
  210. /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
  211. /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
@@ -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
+ }
@@ -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
  }
@@ -2,7 +2,10 @@
2
2
  * Agent Sam local PTY — localhost WebSocket shell, no tunnel, no IAM.
3
3
  * Compatible with iam-pty wire format (raw bytes + JSON resize/slash).
4
4
  */
5
+ import fs from 'node:fs';
5
6
  import http from 'node:http';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
6
9
  import { WebSocketServer } from 'ws';
7
10
 
8
11
  const DEFAULT_PORT = 3099;
@@ -17,8 +20,45 @@ function shellForPlatform() {
17
20
  return process.env.SHELL || '/bin/zsh';
18
21
  }
19
22
 
20
- async function loadPty() {
23
+ export function ensureNodePtySpawnHelperExecutable(options = {}) {
24
+ const platform = options.platform || process.platform;
25
+ const arch = options.arch || process.arch;
26
+ if (platform !== 'darwin') return Object.freeze({ checked: false, changed: false, path: null });
27
+
28
+ const resolveModule = options.resolveModule || ((specifier) => import.meta.resolve(specifier));
29
+ const fsImpl = options.fs || fs;
30
+ const entryUrl = resolveModule('node-pty');
31
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(entryUrl)), '..');
32
+ const helperPath = path.join(packageRoot, 'prebuilds', `darwin-${arch}`, 'spawn-helper');
33
+
34
+ if (!fsImpl.existsSync(helperPath)) {
35
+ return Object.freeze({ checked: true, changed: false, path: helperPath, missing: true });
36
+ }
37
+
38
+ const stat = fsImpl.statSync(helperPath);
39
+ if ((stat.mode & 0o111) !== 0) {
40
+ return Object.freeze({ checked: true, changed: false, path: helperPath });
41
+ }
42
+
21
43
  try {
44
+ fsImpl.chmodSync(helperPath, stat.mode | 0o111);
45
+ } catch (error) {
46
+ const wrapped = new Error(
47
+ `node-pty spawn-helper is not executable and Agent Sam could not repair it at ${helperPath}. ` +
48
+ `Reinstall node-pty with install scripts enabled or make that helper executable. ${error?.message || error}`,
49
+ );
50
+ wrapped.code = 'node_pty_spawn_helper_not_executable';
51
+ wrapped.cause = error;
52
+ throw wrapped;
53
+ }
54
+
55
+ return Object.freeze({ checked: true, changed: true, path: helperPath });
56
+ }
57
+
58
+ async function loadPty(override) {
59
+ if (override?.spawn) return override;
60
+ try {
61
+ ensureNodePtySpawnHelperExecutable();
22
62
  const mod = await import('node-pty');
23
63
  return mod.default || mod;
24
64
  } catch (e) {
@@ -29,12 +69,69 @@ async function loadPty() {
29
69
  }
30
70
 
31
71
  /**
32
- * @param {{ cwd?: string, port?: number, host?: string }} [opts]
72
+ * Attach the portable PTY wire protocol to an already-created WebSocket-like transport.
73
+ * Exported so the release gate can test terminal semantics against an in-memory mock transport.
74
+ */
75
+ export function attachLocalPtySession({ ws, pty, shell, cwd, cols = 80, rows = 24, env = process.env, sessionId } = {}) {
76
+ if (!ws?.on || !ws?.send) throw new TypeError('ws transport with on/send is required');
77
+ if (!pty?.spawn) throw new TypeError('pty transport with spawn is required');
78
+ const term = pty.spawn(shell, [], {
79
+ name: 'xterm-256color',
80
+ cols,
81
+ rows,
82
+ cwd,
83
+ env: { ...env, TERM: 'xterm-256color', AGENTSAM_LOCAL_PTY: '1' },
84
+ });
85
+ const id = sessionId || `local_${Date.now().toString(36)}`;
86
+ const openState = ws.OPEN ?? 1;
87
+ const isOpen = () => ws.readyState == null || ws.readyState === openState;
88
+
89
+ ws.send(JSON.stringify({ type: 'session_id', session_id: id }));
90
+
91
+ term.onData((data) => {
92
+ if (isOpen()) ws.send(data);
93
+ });
94
+
95
+ ws.on('message', (raw) => {
96
+ const text = raw.toString();
97
+ try {
98
+ const msg = JSON.parse(text);
99
+ if (msg.type === 'resize' && msg.cols && msg.rows) {
100
+ term.resize(msg.cols, msg.rows);
101
+ return;
102
+ }
103
+ if (msg.type === 'slash' && msg.line) {
104
+ term.write(`${msg.line}\r`);
105
+ return;
106
+ }
107
+ } catch {
108
+ /* raw PTY input */
109
+ }
110
+ term.write(text);
111
+ });
112
+
113
+ let cleaned = false;
114
+ const cleanup = () => {
115
+ if (cleaned) return;
116
+ cleaned = true;
117
+ try { term.kill(); } catch { /* ignore */ }
118
+ };
119
+ term.onExit(() => {
120
+ if (isOpen() && ws.close) ws.close();
121
+ });
122
+ ws.on('close', cleanup);
123
+ ws.on('error', cleanup);
124
+
125
+ return Object.freeze({ session_id: id, term, cleanup });
126
+ }
127
+
128
+ /**
129
+ * @param {{ cwd?: string, port?: number, host?: string, pty?: { spawn: Function } }} [opts]
33
130
  */
34
131
  export async function startLocalPtyServer(opts = {}) {
35
- const pty = await loadPty();
132
+ const pty = await loadPty(opts.pty);
36
133
  const cwd = opts.cwd || process.cwd();
37
- const port = parsePort(opts.port ?? process.env.PTY_PORT, DEFAULT_PORT);
134
+ const requestedPort = opts.port === 0 ? 0 : parsePort(opts.port ?? process.env.PTY_PORT, DEFAULT_PORT);
38
135
  const host = opts.host || '127.0.0.1';
39
136
  const shell = shellForPlatform();
40
137
 
@@ -47,7 +144,7 @@ export async function startLocalPtyServer(opts = {}) {
47
144
  ok: true,
48
145
  service: 'agentsam-local-pty',
49
146
  cwd,
50
- port,
147
+ port: Number(httpServer.address()?.port || requestedPort),
51
148
  shell,
52
149
  }),
53
150
  );
@@ -65,64 +162,29 @@ export async function startLocalPtyServer(opts = {}) {
65
162
  const cols = parsePort(url.searchParams.get('cols'), 80);
66
163
  const rows = parsePort(url.searchParams.get('rows'), 24);
67
164
 
68
- const term = pty.spawn(shell, [], {
69
- name: 'xterm-256color',
165
+ attachLocalPtySession({
166
+ ws,
167
+ pty,
168
+ shell,
169
+ cwd: sessionCwd,
70
170
  cols,
71
171
  rows,
72
- cwd: sessionCwd,
73
- env: { ...process.env, TERM: 'xterm-256color', AGENTSAM_LOCAL_PTY: '1' },
74
- });
75
-
76
- const sessionId = `local_${Date.now().toString(36)}`;
77
- ws.send(JSON.stringify({ type: 'session_id', session_id: sessionId }));
78
-
79
- term.onData((data) => {
80
- if (ws.readyState === ws.OPEN) ws.send(data);
81
- });
82
-
83
- ws.on('message', (raw) => {
84
- const text = raw.toString();
85
- try {
86
- const msg = JSON.parse(text);
87
- if (msg.type === 'resize' && msg.cols && msg.rows) {
88
- term.resize(msg.cols, msg.rows);
89
- return;
90
- }
91
- if (msg.type === 'slash' && msg.line) {
92
- term.write(`${msg.line}\r`);
93
- return;
94
- }
95
- } catch {
96
- /* raw PTY input */
97
- }
98
- term.write(text);
99
- });
100
-
101
- const cleanup = () => {
102
- try {
103
- term.kill();
104
- } catch {
105
- /* ignore */
106
- }
107
- };
108
- term.onExit(() => {
109
- if (ws.readyState === ws.OPEN) ws.close();
172
+ env: process.env,
110
173
  });
111
- ws.on('close', cleanup);
112
- ws.on('error', cleanup);
113
174
  });
114
175
 
115
176
  await new Promise((resolve) => {
116
- httpServer.listen(port, host, resolve);
177
+ httpServer.listen(requestedPort, host, resolve);
117
178
  });
179
+ const boundPort = Number(httpServer.address()?.port || requestedPort);
118
180
 
119
181
  return {
120
- port,
182
+ port: boundPort,
121
183
  host,
122
184
  cwd,
123
185
  shell,
124
- url: `ws://${host}:${port}`,
125
- healthUrl: `http://${host}:${port}/health`,
186
+ url: `ws://${host}:${boundPort}`,
187
+ healthUrl: `http://${host}:${boundPort}/health`,
126
188
  close: () =>
127
189
  new Promise((resolve, reject) => {
128
190
  wss.close(() => {
@@ -0,0 +1,135 @@
1
+ export const MODEL_CATALOG_SCHEMA = 'agentsam-model-catalog-v1';
2
+
3
+ const ASTRA_SOURCE = 'https://developers.openai.com/api/docs/models/gpt-6-astra';
4
+
5
+ const GPT_6_ASTRA = Object.freeze({
6
+ schema_version: 1,
7
+ model_key: 'openai:gpt-6-astra',
8
+ provider: 'openai',
9
+ provider_model_id: 'gpt-6-astra',
10
+ label: 'GPT-6 Astra',
11
+ context_window: 1_050_000,
12
+ max_output_tokens: 128_000,
13
+ reasoning_efforts: Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']),
14
+ service_tiers: Object.freeze(['default', 'fast', 'flex']),
15
+ capabilities: Object.freeze({
16
+ responses: true,
17
+ streaming: true,
18
+ function_calling: true,
19
+ structured_outputs: true,
20
+ prompt_caching: true,
21
+ compaction: true,
22
+ tool_search: true,
23
+ async_tool_calls: true,
24
+ mid_turn_steering: true,
25
+ configuration_update: true,
26
+ batch: true,
27
+ fast: true,
28
+ flex: true,
29
+ }),
30
+ pricing: Object.freeze({
31
+ currency: 'USD',
32
+ unit: 'per_million_tokens',
33
+ input: 10,
34
+ cached_input: 1,
35
+ cache_write: 12.5,
36
+ output: 50,
37
+ thresholds: Object.freeze([
38
+ Object.freeze({
39
+ input_tokens_gt: 272_000,
40
+ applies_to_full_request: true,
41
+ multipliers: Object.freeze({ input: 2, cached_input: 2, cache_write: 2, output: 1.5 }),
42
+ }),
43
+ ]),
44
+ service_tier_multipliers: Object.freeze({ default: 1, fast: 2, flex: 0.5, batch: 0.5 }),
45
+ source: ASTRA_SOURCE,
46
+ as_of: '2026-09-12',
47
+ }),
48
+ context_policy: Object.freeze({
49
+ target_input_tokens: 120_000,
50
+ compact_at_tokens: 180_000,
51
+ intervene_at_tokens: 220_000,
52
+ max_normal_input_tokens: 250_000,
53
+ pricing_threshold_tokens: 272_000,
54
+ safety_margin_tokens: 22_000,
55
+ }),
56
+ batch: Object.freeze({
57
+ supported: true,
58
+ completion_window: '24h',
59
+ relative_price: 0.5,
60
+ interactive: false,
61
+ }),
62
+ source: Object.freeze({ url: ASTRA_SOURCE, as_of: '2026-09-12' }),
63
+ });
64
+
65
+ export const MODEL_CATALOG = Object.freeze([GPT_6_ASTRA]);
66
+
67
+ function clean(value) {
68
+ return value == null ? '' : String(value).trim();
69
+ }
70
+
71
+ function nonNegativeInteger(value, label) {
72
+ const number = Number(value ?? 0);
73
+ if (!Number.isFinite(number) || number < 0) throw new RangeError(`${label} must be a non-negative number`);
74
+ return Math.floor(number);
75
+ }
76
+
77
+ export function listModelCatalog(options = {}) {
78
+ const provider = clean(options.provider).toLowerCase();
79
+ return MODEL_CATALOG.filter((row) => !provider || row.provider === provider);
80
+ }
81
+
82
+ export function getModelRecord(value) {
83
+ const key = clean(value);
84
+ if (!key) return null;
85
+ return MODEL_CATALOG.find((row) => row.model_key === key || row.provider_model_id === key) || null;
86
+ }
87
+
88
+ export function calculateModelCost(model, usage = {}, options = {}) {
89
+ const record = typeof model === 'string' ? getModelRecord(model) : model;
90
+ if (!record?.pricing) throw new TypeError('model pricing record is required');
91
+
92
+ const totalInput = nonNegativeInteger(usage.input_tokens ?? usage.inputTokens, 'input_tokens');
93
+ const cachedInput = Math.min(totalInput, nonNegativeInteger(usage.cached_input_tokens ?? usage.cachedInputTokens, 'cached_input_tokens'));
94
+ const cacheWrite = Math.min(totalInput - cachedInput, nonNegativeInteger(usage.cache_write_tokens ?? usage.cacheWriteTokens, 'cache_write_tokens'));
95
+ const uncachedInput = Math.max(0, totalInput - cachedInput - cacheWrite);
96
+ const output = nonNegativeInteger(usage.output_tokens ?? usage.outputTokens, 'output_tokens');
97
+ const serviceTier = clean(options.serviceTier || usage.service_tier || usage.serviceTier || 'default') || 'default';
98
+ const tierMultiplier = record.pricing.service_tier_multipliers?.[serviceTier];
99
+ if (!Number.isFinite(tierMultiplier)) throw new RangeError(`unsupported service tier for ${record.provider_model_id}: ${serviceTier}`);
100
+
101
+ const threshold = (record.pricing.thresholds || []).find((row) => totalInput > row.input_tokens_gt) || null;
102
+ const thresholdMultipliers = threshold?.multipliers || {};
103
+ const rates = {
104
+ input: record.pricing.input * (thresholdMultipliers.input || 1) * tierMultiplier,
105
+ cached_input: record.pricing.cached_input * (thresholdMultipliers.cached_input || 1) * tierMultiplier,
106
+ cache_write: record.pricing.cache_write * (thresholdMultipliers.cache_write || 1) * tierMultiplier,
107
+ output: record.pricing.output * (thresholdMultipliers.output || 1) * tierMultiplier,
108
+ };
109
+ const components = {
110
+ input: (uncachedInput * rates.input) / 1_000_000,
111
+ cached_input: (cachedInput * rates.cached_input) / 1_000_000,
112
+ cache_write: (cacheWrite * rates.cache_write) / 1_000_000,
113
+ output: (output * rates.output) / 1_000_000,
114
+ };
115
+ const total = Object.values(components).reduce((sum, value) => sum + value, 0);
116
+
117
+ return Object.freeze({
118
+ model_key: record.model_key,
119
+ service_tier: serviceTier,
120
+ usage: Object.freeze({
121
+ input_tokens: totalInput,
122
+ uncached_input_tokens: uncachedInput,
123
+ cached_input_tokens: cachedInput,
124
+ cache_write_tokens: cacheWrite,
125
+ output_tokens: output,
126
+ }),
127
+ threshold_applied: threshold ? Object.freeze({ ...threshold }) : null,
128
+ rates_per_million: Object.freeze(rates),
129
+ components_usd: Object.freeze(components),
130
+ total_usd: total,
131
+ estimate_kind: usage.estimate_kind === 'provider' ? 'provider' : 'local',
132
+ pricing_source: record.pricing.source,
133
+ pricing_as_of: record.pricing.as_of,
134
+ });
135
+ }