@titan-design/agent 0.3.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);
@@ -251,7 +533,7 @@ var Session = class {
251
533
  if (c.mcpServers) options.mcpServers = c.mcpServers;
252
534
  if (c.hooks) options.hooks = c.hooks;
253
535
  if (c.outputSchema) {
254
- options.outputFormat = { type: "json_schema", schema: toJSONSchema(c.outputSchema) };
536
+ options.outputFormat = { type: "json_schema", schema: toJSONSchema2(c.outputSchema) };
255
537
  }
256
538
  return options;
257
539
  }
@@ -348,24 +630,6 @@ function describeEnd(reason) {
348
630
  return reason.kind === "aborted" ? reason.reason : `no message for ${reason.timeoutMs}ms`;
349
631
  }
350
632
 
351
- // src/harness-contracts.ts
352
- var CODEX_SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
353
- var CODEX_APPROVAL_POLICIES = ["untrusted", "on-failure", "on-request", "never"];
354
- var EXECUTION_CAPABILITIES = [
355
- "fresh_run",
356
- "resume",
357
- "fork",
358
- "structured_output",
359
- "interactive_approvals",
360
- "external_cancellation",
361
- "semantic_interrupt",
362
- "active_turn_steering",
363
- "idle_turn_submission",
364
- "persisted_transcript",
365
- "process_reattachment",
366
- "token_reporting"
367
- ];
368
-
369
633
  // src/preflight.ts
370
634
  import { conversationRef } from "@titan-design/agent-protocol";
371
635
  function preflightHarnessRun(request, descriptor) {
@@ -660,7 +924,7 @@ var CodexExecSession = class {
660
924
  if (this.stopReason === "timeout") return this.failure(timeoutFailure(this.request.wallTimeMs));
661
925
  if (this.stopReason === "abort") return this.failure(abortFailure(this.request.signal));
662
926
  if (this.streamError) return this.failure(this.streamError);
663
- if (exit.exitCode !== 0) return this.failure(exitFailure(exit, this.stderr));
927
+ if (exit.exitCode !== 0) return this.failure(exitFailure2(exit, this.stderr));
664
928
  return this.success();
665
929
  }
666
930
  success() {
@@ -811,7 +1075,7 @@ function cancelledUnknown(cause, request, exit) {
811
1075
  native: { cause, requested, processExit: exit }
812
1076
  };
813
1077
  }
814
- function exitFailure(exit, stderr) {
1078
+ function exitFailure2(exit, stderr) {
815
1079
  return {
816
1080
  kind: "runtime_error",
817
1081
  reason: stderr.trim() || `Codex exited with status ${String(exit.exitCode)}`,
@@ -830,80 +1094,6 @@ function messageOf3(error) {
830
1094
  return error instanceof Error ? error.message : String(error);
831
1095
  }
832
1096
 
833
- // src/codex-process.ts
834
- import { execFile, spawn } from "child_process";
835
- import { mkdtemp, rm, writeFile } from "fs/promises";
836
- import { tmpdir } from "os";
837
- import { join } from "path";
838
- import { createInterface } from "readline";
839
- async function inspectCodexVersion(executablePath, env, options) {
840
- return new Promise((resolve, reject) => {
841
- execFile(
842
- executablePath,
843
- ["--version"],
844
- {
845
- env,
846
- encoding: "utf8",
847
- signal: options.signal,
848
- timeout: Math.max(1, Math.ceil(options.timeoutMs)),
849
- killSignal: "SIGKILL"
850
- },
851
- (error, stdout) => {
852
- if (error) reject(error);
853
- else resolve(stdout);
854
- }
855
- );
856
- });
857
- }
858
- function startCodexProcess(input) {
859
- const child = spawn(input.executablePath, input.args, {
860
- cwd: input.cwd,
861
- env: input.env,
862
- detached: process.platform !== "win32",
863
- stdio: ["ignore", "pipe", "pipe"]
864
- });
865
- return {
866
- stdout: lines(child.stdout),
867
- stderr: lines(child.stderr),
868
- completed: completionOf(child),
869
- terminate: (signal) => terminateProcessGroup(child, signal)
870
- };
871
- }
872
- async function createCodexSchemaFile(schema) {
873
- const directory = await mkdtemp(join(tmpdir(), "titan-codex-schema-"));
874
- const path = join(directory, "output-schema.json");
875
- try {
876
- await writeFile(path, JSON.stringify(schema), { encoding: "utf8", mode: 384 });
877
- return { path, dispose: () => rm(directory, { recursive: true, force: true }) };
878
- } catch (error) {
879
- await rm(directory, { recursive: true, force: true });
880
- throw error;
881
- }
882
- }
883
- async function* lines(stream) {
884
- const reader = createInterface({ input: stream });
885
- for await (const line of reader) yield line;
886
- }
887
- function completionOf(child) {
888
- return new Promise((resolve, reject) => {
889
- child.once("error", reject);
890
- child.once("close", (exitCode, signal) => resolve({ exitCode, signal }));
891
- });
892
- }
893
- function terminateProcessGroup(child, signal) {
894
- if (child.pid && process.platform !== "win32") {
895
- try {
896
- process.kill(-child.pid, signal);
897
- return;
898
- } catch {
899
- }
900
- }
901
- try {
902
- child.kill(signal);
903
- } catch {
904
- }
905
- }
906
-
907
1097
  // src/codex-exec.ts
908
1098
  var DEFAULT_CODEX_EXECUTABLE = "/Applications/ChatGPT.app/Contents/Resources/codex";
909
1099
  var SUPPORTED_CODEX_EXEC_VERSION = "0.154.0-alpha.6.1";
@@ -1792,6 +1982,8 @@ export {
1792
1982
  ANTI_NESTING_VARS,
1793
1983
  AuthMisconfiguredError,
1794
1984
  CLAUDE_CODE_PASSTHROUGH_VARS,
1985
+ CLAUDE_PRINT_KILL_GRACE_MS,
1986
+ CLAUDE_PRINT_UNSUPPORTED_OPTIONS,
1795
1987
  CODEX_APPROVAL_POLICIES,
1796
1988
  CODEX_SANDBOXES,
1797
1989
  DEFAULT_CODEX_EXECUTABLE,
@@ -1806,9 +1998,11 @@ export {
1806
1998
  SUPPORTED_CODEX_EXEC_VERSION,
1807
1999
  assertApiKeySourceAllowed,
1808
2000
  assertAuthEnvOk,
2001
+ buildClaudePrintArgs,
1809
2002
  buildCodexExecArgs,
1810
2003
  classifyResult,
1811
2004
  claudeCodeCapabilities,
2005
+ claudePrintCapabilities,
1812
2006
  codexExecCapabilities,
1813
2007
  createClaudeCodeAdapter,
1814
2008
  createCodexExecAdapter,
@@ -1817,6 +2011,7 @@ export {
1817
2011
  preflightHarnessRun,
1818
2012
  prepareCodexEnv,
1819
2013
  prepareEnv,
2014
+ resolveClaudeBin,
1820
2015
  runAgent,
1821
2016
  sumModelCost,
1822
2017
  usageFromResult