@titan-design/agent 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/run.ts
2
2
  import { query } from "@anthropic-ai/claude-agent-sdk";
3
- import { toJSONSchema } from "zod";
3
+ import { toJSONSchema as toJSONSchema2 } from "zod";
4
4
 
5
5
  // src/env.ts
6
6
  var ANTI_NESTING_VARS = ["CLAUDECODE", "CLAUDE_CODE_SSE_PORT", "CLAUDE_CODE_ENTRYPOINT"];
@@ -64,7 +64,7 @@ function assertAuthEnvOk(env, options = {}) {
64
64
  "pass the env through prepareEnv, or set allowApiKeyBilling to bill the API account on purpose"
65
65
  );
66
66
  }
67
- if (!env.CLAUDE_CODE_OAUTH_TOKEN) {
67
+ if (options.requireOAuthToken !== false && !env.CLAUDE_CODE_OAUTH_TOKEN) {
68
68
  throw new AuthMisconfiguredError(
69
69
  "CLAUDE_CODE_OAUTH_TOKEN is not set; subscription auth will fail",
70
70
  "run `claude setup-token` once and export the printed token"
@@ -84,6 +84,86 @@ function assertApiKeySourceAllowed(observed) {
84
84
  }
85
85
  }
86
86
 
87
+ // src/claude-print.ts
88
+ import { spawn as spawn2 } from "child_process";
89
+ import { accessSync, constants, statSync } from "fs";
90
+ import { delimiter, join as join2 } from "path";
91
+ import { toJSONSchema } from "zod";
92
+
93
+ // src/codex-process.ts
94
+ import { execFile, spawn } from "child_process";
95
+ import { mkdtemp, rm, writeFile } from "fs/promises";
96
+ import { tmpdir } from "os";
97
+ import { join } from "path";
98
+ import { createInterface } from "readline";
99
+ async function inspectCodexVersion(executablePath, env, options) {
100
+ return new Promise((resolve, reject) => {
101
+ execFile(
102
+ executablePath,
103
+ ["--version"],
104
+ {
105
+ env,
106
+ encoding: "utf8",
107
+ signal: options.signal,
108
+ timeout: Math.max(1, Math.ceil(options.timeoutMs)),
109
+ killSignal: "SIGKILL"
110
+ },
111
+ (error, stdout) => {
112
+ if (error) reject(error);
113
+ else resolve(stdout);
114
+ }
115
+ );
116
+ });
117
+ }
118
+ function startCodexProcess(input) {
119
+ const child = spawn(input.executablePath, input.args, {
120
+ cwd: input.cwd,
121
+ env: input.env,
122
+ detached: process.platform !== "win32",
123
+ stdio: ["ignore", "pipe", "pipe"]
124
+ });
125
+ return {
126
+ stdout: lines(child.stdout),
127
+ stderr: lines(child.stderr),
128
+ completed: completionOf(child),
129
+ terminate: (signal) => terminateProcessGroup(child, signal)
130
+ };
131
+ }
132
+ async function createCodexSchemaFile(schema) {
133
+ const directory = await mkdtemp(join(tmpdir(), "titan-codex-schema-"));
134
+ const path = join(directory, "output-schema.json");
135
+ try {
136
+ await writeFile(path, JSON.stringify(schema), { encoding: "utf8", mode: 384 });
137
+ return { path, dispose: () => rm(directory, { recursive: true, force: true }) };
138
+ } catch (error) {
139
+ await rm(directory, { recursive: true, force: true });
140
+ throw error;
141
+ }
142
+ }
143
+ async function* lines(stream) {
144
+ const reader = createInterface({ input: stream });
145
+ for await (const line of reader) yield line;
146
+ }
147
+ function completionOf(child) {
148
+ return new Promise((resolve, reject) => {
149
+ child.once("error", reject);
150
+ child.once("close", (exitCode, signal) => resolve({ exitCode, signal }));
151
+ });
152
+ }
153
+ function terminateProcessGroup(child, signal) {
154
+ if (child.pid && process.platform !== "win32") {
155
+ try {
156
+ process.kill(-child.pid, signal);
157
+ return;
158
+ } catch {
159
+ }
160
+ }
161
+ try {
162
+ child.kill(signal);
163
+ } catch {
164
+ }
165
+ }
166
+
87
167
  // src/failures.ts
88
168
  var RATE_LIMIT_PATTERNS = [
89
169
  /you'?ve hit your (session|weekly|opus|sonnet) limit/i,
@@ -146,18 +226,220 @@ function sumModelCost(modelUsage) {
146
226
  return Object.values(modelUsage).reduce((total, m) => total + (m.costUSD ?? 0), 0);
147
227
  }
148
228
 
229
+ // src/harness-contracts.ts
230
+ var CLAUDE_PRINT_UNSUPPORTED_OPTIONS = [
231
+ "allowedTools",
232
+ "disallowedTools",
233
+ "permissionMode",
234
+ "resumeSessionId",
235
+ "agents",
236
+ "mcpServers",
237
+ "hooks"
238
+ ];
239
+ var CODEX_SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
240
+ var CODEX_APPROVAL_POLICIES = ["untrusted", "on-failure", "on-request", "never"];
241
+ var EXECUTION_CAPABILITIES = [
242
+ "fresh_run",
243
+ "resume",
244
+ "fork",
245
+ "structured_output",
246
+ "interactive_approvals",
247
+ "external_cancellation",
248
+ "semantic_interrupt",
249
+ "active_turn_steering",
250
+ "idle_turn_submission",
251
+ "persisted_transcript",
252
+ "process_reattachment",
253
+ "token_reporting"
254
+ ];
255
+
256
+ // src/claude-print.ts
257
+ var CLAUDE_PRINT_KILL_GRACE_MS = 5e3;
258
+ var DEFAULT_TIMEOUT_MS = 6e5;
259
+ var EMPTY_MCP_CONFIG = '{"mcpServers":{}}';
260
+ var LOGIN_FAILURE = /not logged in|please run \/login|invalid api key/i;
261
+ function claudePrintCapabilities() {
262
+ const capabilities = Object.fromEntries(EXECUTION_CAPABILITIES.map((capability) => [capability, {
263
+ status: "unsupported",
264
+ reason: "claude -p runs one turn with no tools, hooks, MCP or resume"
265
+ }]));
266
+ capabilities.fresh_run = { status: "supported", evidence: "spawns claude -p --max-turns 1 per call" };
267
+ capabilities.structured_output = { status: "supported", evidence: "--json-schema, then a local zod parse" };
268
+ capabilities.external_cancellation = { status: "supported", evidence: "abort kills the child process group" };
269
+ capabilities.token_reporting = { status: "supported", evidence: "usage and modelUsage from the JSON result" };
270
+ return {
271
+ harness: "claude-print",
272
+ adapter: { name: "claude-print", version: "1" },
273
+ capabilities,
274
+ limits: [{ unit: "milliseconds", scope: "execution", enforcement: "hard", assessment: {
275
+ status: "supported",
276
+ evidence: "inactivityTimeoutMs is a wall deadline that kills the child process group"
277
+ } }]
278
+ };
279
+ }
280
+ function assertClaudePrintConfig(config) {
281
+ const field = CLAUDE_PRINT_UNSUPPORTED_OPTIONS.find((name) => config[name] !== void 0) ?? (config.tools?.length ? "tools" : void 0) ?? (config.settingSources?.length ? "settingSources" : void 0);
282
+ if (field) throw new TypeError(`${field} is not supported by the claude-print harness (one turn, no tools, hooks or MCP)`);
283
+ }
284
+ function resolveClaudeBin(env) {
285
+ if (env.CLAUDE_BIN) return env.CLAUDE_BIN;
286
+ for (const dir of (env.PATH ?? "").split(delimiter)) {
287
+ if (dir && isExecutableFile(join2(dir, "claude"))) return join2(dir, "claude");
288
+ }
289
+ return void 0;
290
+ }
291
+ function isExecutableFile(path) {
292
+ try {
293
+ accessSync(path, constants.X_OK);
294
+ return statSync(path).isFile();
295
+ } catch {
296
+ return false;
297
+ }
298
+ }
299
+ function buildClaudePrintArgs(config) {
300
+ const args = [
301
+ "-p",
302
+ "--output-format",
303
+ "json",
304
+ "--tools",
305
+ "",
306
+ "--strict-mcp-config",
307
+ "--mcp-config",
308
+ EMPTY_MCP_CONFIG,
309
+ "--setting-sources",
310
+ "",
311
+ "--max-turns",
312
+ "1",
313
+ "--max-budget-usd",
314
+ String(config.maxBudgetUsd)
315
+ ];
316
+ if (config.model) args.push("--model", config.model);
317
+ if (config.systemPrompt !== void 0) args.push("--system-prompt", config.systemPrompt);
318
+ if (config.outputSchema) args.push("--json-schema", cliJsonSchema(config.outputSchema));
319
+ return args;
320
+ }
321
+ function cliJsonSchema(schema) {
322
+ const body = { ...toJSONSchema(schema) };
323
+ delete body.$schema;
324
+ return JSON.stringify(body);
325
+ }
326
+ async function runClaudePrint(config, env, clock) {
327
+ if (config.signal?.aborted) return { ok: false, failure: aborted(config.signal) };
328
+ const bin = resolveClaudeBin(env);
329
+ if (!bin) return { ok: false, failure: { kind: "runtime_error", reason: "claude binary not found on PATH; set CLAUDE_BIN" } };
330
+ const child = spawn2(bin, buildClaudePrintArgs(config), {
331
+ cwd: config.cwd,
332
+ env,
333
+ detached: process.platform !== "win32",
334
+ stdio: ["pipe", "pipe", "pipe"]
335
+ });
336
+ const outcome = await supervise(child, config);
337
+ return settle(outcome, config, clock.now() - clock.startedAt);
338
+ }
339
+ function supervise(child, config) {
340
+ const timeoutMs = config.inactivityTimeoutMs ?? DEFAULT_TIMEOUT_MS;
341
+ const output = collectOutput(child);
342
+ let ended;
343
+ const stop = (failure) => {
344
+ ended ??= failure;
345
+ terminateProcessGroup(child, "SIGTERM");
346
+ setTimeout(() => terminateProcessGroup(child, "SIGKILL"), CLAUDE_PRINT_KILL_GRACE_MS).unref();
347
+ };
348
+ const timer = setTimeout(() => stop({ kind: "inactivity_timeout", reason: `claude -p ran past ${timeoutMs}ms`, timeoutMs }), timeoutMs);
349
+ const onAbort = () => stop(aborted(config.signal));
350
+ config.signal?.addEventListener("abort", onAbort, { once: true });
351
+ child.stdin?.on("error", () => {
352
+ });
353
+ child.stdin?.end(config.prompt);
354
+ return new Promise((resolve) => {
355
+ const finish = (code, signal, spawnError) => {
356
+ clearTimeout(timer);
357
+ config.signal?.removeEventListener("abort", onAbort);
358
+ resolve({ code, signal, ...output(), ...spawnError ? { spawnError } : {}, ...ended ? { ended } : {} });
359
+ };
360
+ child.once("error", (error) => finish(null, null, error));
361
+ child.once("close", (code, signal) => finish(code, signal));
362
+ });
363
+ }
364
+ function collectOutput(child) {
365
+ const stdout = [];
366
+ const stderr = [];
367
+ child.stdout?.on("data", (chunk) => stdout.push(chunk));
368
+ child.stderr?.on("data", (chunk) => stderr.push(chunk));
369
+ return () => ({ stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
370
+ }
371
+ function settle(outcome, config, durationMs) {
372
+ if (outcome.ended) return { ok: false, failure: outcome.ended };
373
+ if (outcome.spawnError) return { ok: false, failure: { kind: "runtime_error", reason: `claude -p failed to start: ${outcome.spawnError.message}`, raw: outcome.spawnError } };
374
+ const result = parseResult(outcome.stdout);
375
+ if (!result) return { ok: false, failure: exitFailure(outcome, "printed no JSON result") };
376
+ config.onMessage?.(result);
377
+ const usage = usageFromResult(result, durationMs);
378
+ const failure = loginFailure(result) ?? classifyResult(result) ?? (outcome.code === 0 ? void 0 : exitFailure(outcome, "reported success"));
379
+ const sessionId = typeof result.session_id === "string" ? result.session_id : void 0;
380
+ if (failure) return { ok: false, failure, ...sessionId ? { sessionId } : {}, usage };
381
+ if (!sessionId) return { ok: false, failure: { kind: "runtime_error", reason: "the JSON result carried no session_id", raw: result }, usage };
382
+ const output = readOutput(result, config);
383
+ if ("failure" in output) return { ok: false, failure: output.failure, sessionId, usage };
384
+ return { ok: true, output: output.value, sessionId, usage, init: initFrom(result) };
385
+ }
386
+ function parseResult(stdout) {
387
+ try {
388
+ const parsed = JSON.parse(stdout);
389
+ const isResult = typeof parsed === "object" && parsed !== null && parsed.type === "result";
390
+ return isResult ? parsed : void 0;
391
+ } catch {
392
+ return void 0;
393
+ }
394
+ }
395
+ function exitFailure(outcome, what) {
396
+ const exit = outcome.signal ? `signal ${outcome.signal}` : `code ${String(outcome.code)}`;
397
+ const stderr = outcome.stderr.trim();
398
+ return { kind: "runtime_error", reason: `claude -p exited with ${exit} and ${what}${stderr ? `: ${stderr}` : ""}`, raw: outcome };
399
+ }
400
+ function loginFailure(result) {
401
+ const text = result.subtype === "success" ? result.result : (result.errors ?? []).join("\n");
402
+ if (!result.is_error || !LOGIN_FAILURE.test(text)) return void 0;
403
+ return { kind: "auth_misconfigured", reason: text, hint: "run `claude` once interactively and /login", raw: result };
404
+ }
405
+ function readOutput(result, config) {
406
+ const text = result.subtype === "success" ? result.result : "";
407
+ if (!config.outputSchema) return { value: text };
408
+ const raw = (result.subtype === "success" ? result.structured_output : void 0) ?? parseJson(text);
409
+ const parsed = config.outputSchema.safeParse(raw);
410
+ if (parsed.success) return { value: parsed.data };
411
+ return { failure: { kind: "schema_invalid", reason: "structured output did not match the supplied schema", error: parsed.error, raw } };
412
+ }
413
+ function parseJson(text) {
414
+ try {
415
+ return JSON.parse(text);
416
+ } catch {
417
+ return void 0;
418
+ }
419
+ }
420
+ function initFrom(result) {
421
+ const models = Object.keys(result.modelUsage ?? {});
422
+ return { apiKeySource: void 0, model: models.length === 1 ? models[0] : void 0, tools: [], permissionMode: void 0, claudeCodeVersion: void 0 };
423
+ }
424
+ function aborted(signal) {
425
+ return { kind: "aborted", reason: String(signal?.reason ?? "aborted by caller") };
426
+ }
427
+
149
428
  // src/run.ts
150
429
  var DEFAULT_INACTIVITY_MS = 6e5;
151
430
  async function runAgent(config, deps = {}) {
152
431
  assertBudgets(config);
432
+ if (config.harness === "claude-print") assertClaudePrintConfig(config);
153
433
  const now = deps.now ?? Date.now;
154
434
  const startedAt = now();
435
+ const print = config.harness === "claude-print";
155
436
  const env = prepareEnv(deps.env ?? process.env, { allowApiKeyBilling: config.allowApiKeyBilling });
156
437
  try {
157
- assertAuthEnvOk(env, { allowApiKeyBilling: config.allowApiKeyBilling });
438
+ assertAuthEnvOk(env, { allowApiKeyBilling: config.allowApiKeyBilling, requireOAuthToken: !print });
158
439
  } catch (error) {
159
440
  return { ok: false, failure: authFailure(error) };
160
441
  }
442
+ if (print) return runClaudePrint(config, env, { now, startedAt });
161
443
  const session = new Session(config, env, deps);
162
444
  try {
163
445
  return await session.run(startedAt);
@@ -242,6 +524,8 @@ var Session = class {
242
524
  settingSources: c.settingSources ?? []
243
525
  };
244
526
  if (c.model) options.model = c.model;
527
+ if (c.tools) options.tools = c.tools;
528
+ if (c.systemPrompt !== void 0) options.systemPrompt = c.systemPrompt;
245
529
  if (c.allowedTools) options.allowedTools = c.allowedTools;
246
530
  if (c.disallowedTools) options.disallowedTools = c.disallowedTools;
247
531
  if (c.resumeSessionId) options.resume = c.resumeSessionId;
@@ -249,7 +533,7 @@ var Session = class {
249
533
  if (c.mcpServers) options.mcpServers = c.mcpServers;
250
534
  if (c.hooks) options.hooks = c.hooks;
251
535
  if (c.outputSchema) {
252
- options.outputFormat = { type: "json_schema", schema: toJSONSchema(c.outputSchema) };
536
+ options.outputFormat = { type: "json_schema", schema: toJSONSchema2(c.outputSchema) };
253
537
  }
254
538
  return options;
255
539
  }
@@ -346,24 +630,6 @@ function describeEnd(reason) {
346
630
  return reason.kind === "aborted" ? reason.reason : `no message for ${reason.timeoutMs}ms`;
347
631
  }
348
632
 
349
- // src/harness-contracts.ts
350
- var CODEX_SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
351
- var CODEX_APPROVAL_POLICIES = ["untrusted", "on-failure", "on-request", "never"];
352
- var EXECUTION_CAPABILITIES = [
353
- "fresh_run",
354
- "resume",
355
- "fork",
356
- "structured_output",
357
- "interactive_approvals",
358
- "external_cancellation",
359
- "semantic_interrupt",
360
- "active_turn_steering",
361
- "idle_turn_submission",
362
- "persisted_transcript",
363
- "process_reattachment",
364
- "token_reporting"
365
- ];
366
-
367
633
  // src/preflight.ts
368
634
  import { conversationRef } from "@titan-design/agent-protocol";
369
635
  function preflightHarnessRun(request, descriptor) {
@@ -658,7 +924,7 @@ var CodexExecSession = class {
658
924
  if (this.stopReason === "timeout") return this.failure(timeoutFailure(this.request.wallTimeMs));
659
925
  if (this.stopReason === "abort") return this.failure(abortFailure(this.request.signal));
660
926
  if (this.streamError) return this.failure(this.streamError);
661
- if (exit.exitCode !== 0) return this.failure(exitFailure(exit, this.stderr));
927
+ if (exit.exitCode !== 0) return this.failure(exitFailure2(exit, this.stderr));
662
928
  return this.success();
663
929
  }
664
930
  success() {
@@ -809,7 +1075,7 @@ function cancelledUnknown(cause, request, exit) {
809
1075
  native: { cause, requested, processExit: exit }
810
1076
  };
811
1077
  }
812
- function exitFailure(exit, stderr) {
1078
+ function exitFailure2(exit, stderr) {
813
1079
  return {
814
1080
  kind: "runtime_error",
815
1081
  reason: stderr.trim() || `Codex exited with status ${String(exit.exitCode)}`,
@@ -828,80 +1094,6 @@ function messageOf3(error) {
828
1094
  return error instanceof Error ? error.message : String(error);
829
1095
  }
830
1096
 
831
- // src/codex-process.ts
832
- import { execFile, spawn } from "child_process";
833
- import { mkdtemp, rm, writeFile } from "fs/promises";
834
- import { tmpdir } from "os";
835
- import { join } from "path";
836
- import { createInterface } from "readline";
837
- async function inspectCodexVersion(executablePath, env, options) {
838
- return new Promise((resolve, reject) => {
839
- execFile(
840
- executablePath,
841
- ["--version"],
842
- {
843
- env,
844
- encoding: "utf8",
845
- signal: options.signal,
846
- timeout: Math.max(1, Math.ceil(options.timeoutMs)),
847
- killSignal: "SIGKILL"
848
- },
849
- (error, stdout) => {
850
- if (error) reject(error);
851
- else resolve(stdout);
852
- }
853
- );
854
- });
855
- }
856
- function startCodexProcess(input) {
857
- const child = spawn(input.executablePath, input.args, {
858
- cwd: input.cwd,
859
- env: input.env,
860
- detached: process.platform !== "win32",
861
- stdio: ["ignore", "pipe", "pipe"]
862
- });
863
- return {
864
- stdout: lines(child.stdout),
865
- stderr: lines(child.stderr),
866
- completed: completionOf(child),
867
- terminate: (signal) => terminateProcessGroup(child, signal)
868
- };
869
- }
870
- async function createCodexSchemaFile(schema) {
871
- const directory = await mkdtemp(join(tmpdir(), "titan-codex-schema-"));
872
- const path = join(directory, "output-schema.json");
873
- try {
874
- await writeFile(path, JSON.stringify(schema), { encoding: "utf8", mode: 384 });
875
- return { path, dispose: () => rm(directory, { recursive: true, force: true }) };
876
- } catch (error) {
877
- await rm(directory, { recursive: true, force: true });
878
- throw error;
879
- }
880
- }
881
- async function* lines(stream) {
882
- const reader = createInterface({ input: stream });
883
- for await (const line of reader) yield line;
884
- }
885
- function completionOf(child) {
886
- return new Promise((resolve, reject) => {
887
- child.once("error", reject);
888
- child.once("close", (exitCode, signal) => resolve({ exitCode, signal }));
889
- });
890
- }
891
- function terminateProcessGroup(child, signal) {
892
- if (child.pid && process.platform !== "win32") {
893
- try {
894
- process.kill(-child.pid, signal);
895
- return;
896
- } catch {
897
- }
898
- }
899
- try {
900
- child.kill(signal);
901
- } catch {
902
- }
903
- }
904
-
905
1097
  // src/codex-exec.ts
906
1098
  var DEFAULT_CODEX_EXECUTABLE = "/Applications/ChatGPT.app/Contents/Resources/codex";
907
1099
  var SUPPORTED_CODEX_EXEC_VERSION = "0.154.0-alpha.6.1";
@@ -1790,6 +1982,8 @@ export {
1790
1982
  ANTI_NESTING_VARS,
1791
1983
  AuthMisconfiguredError,
1792
1984
  CLAUDE_CODE_PASSTHROUGH_VARS,
1985
+ CLAUDE_PRINT_KILL_GRACE_MS,
1986
+ CLAUDE_PRINT_UNSUPPORTED_OPTIONS,
1793
1987
  CODEX_APPROVAL_POLICIES,
1794
1988
  CODEX_SANDBOXES,
1795
1989
  DEFAULT_CODEX_EXECUTABLE,
@@ -1804,9 +1998,11 @@ export {
1804
1998
  SUPPORTED_CODEX_EXEC_VERSION,
1805
1999
  assertApiKeySourceAllowed,
1806
2000
  assertAuthEnvOk,
2001
+ buildClaudePrintArgs,
1807
2002
  buildCodexExecArgs,
1808
2003
  classifyResult,
1809
2004
  claudeCodeCapabilities,
2005
+ claudePrintCapabilities,
1810
2006
  codexExecCapabilities,
1811
2007
  createClaudeCodeAdapter,
1812
2008
  createCodexExecAdapter,
@@ -1815,6 +2011,7 @@ export {
1815
2011
  preflightHarnessRun,
1816
2012
  prepareCodexEnv,
1817
2013
  prepareEnv,
2014
+ resolveClaudeBin,
1818
2015
  runAgent,
1819
2016
  sumModelCost,
1820
2017
  usageFromResult