topchester-ai 0.53.0 → 0.55.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/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as runTopchesterCli } from "./cli-CqFhOvlt.mjs";
2
+ import { t as runTopchesterCli } from "./cli-AJknxj59.mjs";
3
3
  //#region src/bin.ts
4
4
  await runTopchesterCli();
5
5
  //#endregion
@@ -127,13 +127,13 @@ function extractAccountIdFromClaims(claims) {
127
127
  const flatClaim = getStringProperty(claims, "chatgpt_account_id");
128
128
  if (flatClaim) return flatClaim;
129
129
  const authClaims = claims["https://api.openai.com/auth"];
130
- if (isPlainObject$2(authClaims)) {
130
+ if (isPlainObject$3(authClaims)) {
131
131
  const namespacedClaim = getStringProperty(authClaims, "chatgpt_account_id");
132
132
  if (namespacedClaim) return namespacedClaim;
133
133
  }
134
134
  const organizations = claims.organizations;
135
135
  if (Array.isArray(organizations)) {
136
- const firstOrganization = organizations.find(isPlainObject$2);
136
+ const firstOrganization = organizations.find(isPlainObject$3);
137
137
  const organizationId = firstOrganization ? getStringProperty(firstOrganization, "id") : void 0;
138
138
  if (organizationId) return organizationId;
139
139
  }
@@ -143,7 +143,7 @@ function parseJwtClaims(token) {
143
143
  if (!parts || parts.length !== 3 || !parts[1]) return;
144
144
  try {
145
145
  const parsed = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
146
- return isPlainObject$2(parsed) ? parsed : void 0;
146
+ return isPlainObject$3(parsed) ? parsed : void 0;
147
147
  } catch {
148
148
  return;
149
149
  }
@@ -190,7 +190,7 @@ async function readJsonObject(response, label) {
190
190
  } catch (error) {
191
191
  throw new CodexAuthError("invalid_response", `Invalid Codex ${label}: ${formatErrorMessage$4(error)}.`);
192
192
  }
193
- if (!isPlainObject$2(parsed)) throw new CodexAuthError("invalid_response", `Invalid Codex ${label}: expected an object.`);
193
+ if (!isPlainObject$3(parsed)) throw new CodexAuthError("invalid_response", `Invalid Codex ${label}: expected an object.`);
194
194
  return parsed;
195
195
  }
196
196
  function isPendingDeviceAuthResponse(response) {
@@ -214,7 +214,7 @@ function optionalPositiveNumber(value) {
214
214
  const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
215
215
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
216
216
  }
217
- function isPlainObject$2(value) {
217
+ function isPlainObject$3(value) {
218
218
  return typeof value === "object" && value !== null && !Array.isArray(value);
219
219
  }
220
220
  function formatErrorMessage$4(error) {
@@ -249,7 +249,7 @@ async function readAuthStore(options = {}) {
249
249
  try {
250
250
  source = await readFile(path, "utf8");
251
251
  } catch (error) {
252
- if (isNodeError$4(error) && error.code === "ENOENT") return createEmptyAuthStore();
252
+ if (isNodeError$5(error) && error.code === "ENOENT") return createEmptyAuthStore();
253
253
  throw error;
254
254
  }
255
255
  let parsed;
@@ -297,7 +297,7 @@ async function getAuthStoreStatus(options = {}) {
297
297
  try {
298
298
  await stat(path);
299
299
  } catch (error) {
300
- if (isNodeError$4(error) && error.code === "ENOENT") return {
300
+ if (isNodeError$5(error) && error.code === "ENOENT") return {
301
301
  path,
302
302
  exists: false,
303
303
  providers: []
@@ -325,13 +325,13 @@ async function getAuthStoreStatus(options = {}) {
325
325
  }
326
326
  }
327
327
  function parseAuthStore(path, value) {
328
- if (!isPlainObject$1(value)) throw new AuthStoreError(path, "<root>: expected an object");
328
+ if (!isPlainObject$2(value)) throw new AuthStoreError(path, "<root>: expected an object");
329
329
  if (value.version !== 1) throw new AuthStoreError(path, "version: expected 1");
330
- if (!isPlainObject$1(value.providers)) throw new AuthStoreError(path, "providers: expected an object");
330
+ if (!isPlainObject$2(value.providers)) throw new AuthStoreError(path, "providers: expected an object");
331
331
  const providers = {};
332
332
  for (const [providerId, providerRecord] of Object.entries(value.providers)) {
333
333
  if (!providerId) throw new AuthStoreError(path, "providers: provider id must not be empty");
334
- if (!isPlainObject$1(providerRecord)) throw new AuthStoreError(path, `providers.${providerId}: expected an object`);
334
+ if (!isPlainObject$2(providerRecord)) throw new AuthStoreError(path, `providers.${providerId}: expected an object`);
335
335
  providers[providerId] = parseProviderRecord(path, providerId, providerRecord);
336
336
  }
337
337
  return {
@@ -388,14 +388,14 @@ function redactProviderStatus(id, record, now) {
388
388
  }
389
389
  async function setModeIfSupported(path, mode) {
390
390
  await chmod(path, mode).catch((error) => {
391
- if (isNodeError$4(error) && (error.code === "ENOSYS" || error.code === "EPERM" || error.code === "EINVAL")) return;
391
+ if (isNodeError$5(error) && (error.code === "ENOSYS" || error.code === "EPERM" || error.code === "EINVAL")) return;
392
392
  throw error;
393
393
  });
394
394
  }
395
- function isPlainObject$1(value) {
395
+ function isPlainObject$2(value) {
396
396
  return typeof value === "object" && value !== null && !Array.isArray(value);
397
397
  }
398
- function isNodeError$4(error) {
398
+ function isNodeError$5(error) {
399
399
  return error instanceof Error && "code" in error;
400
400
  }
401
401
  function formatErrorMessage$3(error) {
@@ -1299,7 +1299,7 @@ async function statExistingFile(resolvedPath, originalPath) {
1299
1299
  try {
1300
1300
  fileStat = await stat(resolvedPath);
1301
1301
  } catch (error) {
1302
- if (isNodeError$3(error) && error.code === "ENOENT") throw new Error(`edit_file can only edit existing files: ${originalPath}`);
1302
+ if (isNodeError$4(error) && error.code === "ENOENT") throw new Error(`edit_file can only edit existing files: ${originalPath}`);
1303
1303
  throw error;
1304
1304
  }
1305
1305
  if (!fileStat.isFile()) throw new Error(`edit_file can only edit regular files: ${originalPath}`);
@@ -1341,7 +1341,7 @@ function summarizeDiff(diff) {
1341
1341
  }
1342
1342
  return `+${added}/-${removed}`;
1343
1343
  }
1344
- function isNodeError$3(error) {
1344
+ function isNodeError$4(error) {
1345
1345
  return error instanceof Error && "code" in error;
1346
1346
  }
1347
1347
  function normalizeEdits(edits) {
@@ -4859,7 +4859,7 @@ async function statTarget(path) {
4859
4859
  try {
4860
4860
  return await stat(path);
4861
4861
  } catch (error) {
4862
- if (isNodeError$2(error) && error.code === "ENOENT") return;
4862
+ if (isNodeError$3(error) && error.code === "ENOENT") return;
4863
4863
  throw error;
4864
4864
  }
4865
4865
  }
@@ -4869,7 +4869,7 @@ async function ensureParentDirectory(workspaceRoot, path, relativePath, createPa
4869
4869
  if (!(await stat(parent)).isDirectory()) throw new Error(`write_file parent path is not a directory: ${dirname(relativePath)}`);
4870
4870
  return [];
4871
4871
  } catch (error) {
4872
- if (!isNodeError$2(error) || error.code !== "ENOENT") throw error;
4872
+ if (!isNodeError$3(error) || error.code !== "ENOENT") throw error;
4873
4873
  }
4874
4874
  if (!createParentDirs) throw new Error(`write_file parent directory does not exist: ${dirname(relativePath)}`);
4875
4875
  const createdParentDirs = await collectMissingParentDirs(workspaceRoot, parent);
@@ -4883,7 +4883,7 @@ async function collectMissingParentDirs(workspaceRoot, parent) {
4883
4883
  if (!(await stat(current)).isDirectory()) throw new Error(`write_file parent path is not a directory: ${relative(workspaceRoot, current)}`);
4884
4884
  break;
4885
4885
  } catch (error) {
4886
- if (!isNodeError$2(error) || error.code !== "ENOENT") throw error;
4886
+ if (!isNodeError$3(error) || error.code !== "ENOENT") throw error;
4887
4887
  missing.push(relative(workspaceRoot, current));
4888
4888
  current = dirname(current);
4889
4889
  }
@@ -4924,7 +4924,7 @@ function countLogicalLines$1(content) {
4924
4924
  const withoutTrailingLineEnding = content.replace(/\r?\n$/u, "");
4925
4925
  return withoutTrailingLineEnding.length === 0 ? 1 : withoutTrailingLineEnding.split(/\r?\n/u).length;
4926
4926
  }
4927
- function isNodeError$2(error) {
4927
+ function isNodeError$3(error) {
4928
4928
  return error instanceof Error && "code" in error;
4929
4929
  }
4930
4930
  //#endregion
@@ -5335,6 +5335,7 @@ function createCodexProviderFetch(options = {}) {
5335
5335
  const providerId = options.providerId ?? "codex";
5336
5336
  const upstreamFetch = options.fetch ?? fetch;
5337
5337
  return (async (input, init) => {
5338
+ const request = await rewriteCodexRequest(input, init);
5338
5339
  const auth = await resolveCodexAuth({
5339
5340
  ...options,
5340
5341
  providerId,
@@ -5344,10 +5345,13 @@ function createCodexProviderFetch(options = {}) {
5344
5345
  headers.delete("authorization");
5345
5346
  headers.set("Authorization", `Bearer ${auth.accessToken}`);
5346
5347
  if (auth.accountId) headers.set("ChatGPT-Account-Id", auth.accountId);
5347
- return upstreamFetch(rewriteCodexRequestUrl(input), {
5348
- ...init,
5348
+ const response = await upstreamFetch(request.input, {
5349
+ ...request.init,
5349
5350
  headers
5350
5351
  });
5352
+ if (request.responseMode === "chat-json") return codexResponsesSseToChatJson(response);
5353
+ if (request.responseMode === "chat-sse") return codexResponsesSseToChatSse(response);
5354
+ return response;
5351
5355
  });
5352
5356
  }
5353
5357
  function rewriteCodexRequestUrl(input) {
@@ -5360,6 +5364,193 @@ function rewriteCodexRequestUrl(input) {
5360
5364
  }
5361
5365
  return input;
5362
5366
  }
5367
+ async function rewriteCodexRequest(input, init) {
5368
+ const rewrittenUrl = rewriteCodexRequestUrl(input);
5369
+ const body = await readJsonBody(init?.body);
5370
+ if (!isChatCompletionsBody(body)) return {
5371
+ input: rewrittenUrl,
5372
+ ...init ? { init } : {}
5373
+ };
5374
+ const stream = body.stream === true;
5375
+ const codexBody = chatCompletionsBodyToCodexResponsesBody(body);
5376
+ const headers = new Headers(init?.headers);
5377
+ headers.set("Content-Type", "application/json");
5378
+ return {
5379
+ input: rewrittenUrl,
5380
+ init: {
5381
+ ...init,
5382
+ headers,
5383
+ body: JSON.stringify(codexBody)
5384
+ },
5385
+ responseMode: stream ? "chat-sse" : "chat-json"
5386
+ };
5387
+ }
5388
+ function chatCompletionsBodyToCodexResponsesBody(body) {
5389
+ const instructions = body.messages.filter((message) => message.role === "system" || message.role === "developer").map((message) => messageContentToText(message.content)).filter((text) => text.trim().length > 0).join("\n\n");
5390
+ const input = body.messages.filter((message) => message.role !== "system" && message.role !== "developer").map(chatMessageToResponseInputItem).filter((item) => item !== void 0);
5391
+ return {
5392
+ model: body.model,
5393
+ instructions: instructions || "You are a helpful assistant.",
5394
+ input,
5395
+ store: false,
5396
+ stream: true,
5397
+ ...typeof body.temperature === "number" ? { temperature: body.temperature } : {},
5398
+ ...typeof body.top_p === "number" ? { top_p: body.top_p } : {},
5399
+ ...typeof body.max_tokens === "number" ? { max_output_tokens: body.max_tokens } : {}
5400
+ };
5401
+ }
5402
+ function chatMessageToResponseInputItem(message) {
5403
+ const text = messageContentToText(message.content);
5404
+ if (!text.trim()) return;
5405
+ return {
5406
+ type: "message",
5407
+ role: message.role === "assistant" ? "assistant" : "user",
5408
+ content: [{
5409
+ type: message.role === "assistant" ? "output_text" : "input_text",
5410
+ text
5411
+ }]
5412
+ };
5413
+ }
5414
+ async function codexResponsesSseToChatJson(response) {
5415
+ if (!response.ok) return response;
5416
+ const parsed = parseCodexResponsesSse(await response.text());
5417
+ const headers = new Headers(response.headers);
5418
+ headers.set("Content-Type", "application/json");
5419
+ return new Response(JSON.stringify({
5420
+ id: parsed.id ?? "codex-response",
5421
+ object: "chat.completion",
5422
+ created: parsed.created ?? Math.floor(Date.now() / 1e3),
5423
+ model: parsed.model ?? "codex",
5424
+ choices: [{
5425
+ index: 0,
5426
+ finish_reason: "stop",
5427
+ message: {
5428
+ role: "assistant",
5429
+ content: parsed.text
5430
+ }
5431
+ }],
5432
+ ...parsed.usage ? { usage: parsed.usage } : {}
5433
+ }), {
5434
+ status: response.status,
5435
+ statusText: response.statusText,
5436
+ headers
5437
+ });
5438
+ }
5439
+ async function codexResponsesSseToChatSse(response) {
5440
+ if (!response.ok) return response;
5441
+ const parsed = parseCodexResponsesSse(await response.text());
5442
+ const encoder = new TextEncoder();
5443
+ const id = parsed.id ?? "codex-response";
5444
+ const created = parsed.created ?? Math.floor(Date.now() / 1e3);
5445
+ const model = parsed.model ?? "codex";
5446
+ const chunks = [
5447
+ {
5448
+ id,
5449
+ object: "chat.completion.chunk",
5450
+ created,
5451
+ model,
5452
+ choices: [{
5453
+ index: 0,
5454
+ delta: { role: "assistant" },
5455
+ finish_reason: null
5456
+ }]
5457
+ },
5458
+ ...(parsed.text.match(/[\s\S]{1,2048}/gu) ?? []).map((delta) => ({
5459
+ id,
5460
+ object: "chat.completion.chunk",
5461
+ created,
5462
+ model,
5463
+ choices: [{
5464
+ index: 0,
5465
+ delta: { content: delta },
5466
+ finish_reason: null
5467
+ }]
5468
+ })),
5469
+ {
5470
+ id,
5471
+ object: "chat.completion.chunk",
5472
+ created,
5473
+ model,
5474
+ choices: [{
5475
+ index: 0,
5476
+ delta: {},
5477
+ finish_reason: "stop"
5478
+ }],
5479
+ ...parsed.usage ? { usage: parsed.usage } : {}
5480
+ }
5481
+ ];
5482
+ const stream = new ReadableStream({ start(controller) {
5483
+ for (const chunk of chunks) controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
5484
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
5485
+ controller.close();
5486
+ } });
5487
+ const headers = new Headers(response.headers);
5488
+ headers.set("Content-Type", "text/event-stream");
5489
+ return new Response(stream, {
5490
+ status: response.status,
5491
+ statusText: response.statusText,
5492
+ headers
5493
+ });
5494
+ }
5495
+ function parseCodexResponsesSse(source) {
5496
+ let text = "";
5497
+ let response;
5498
+ for (const event of source.split(/\n\n/u)) {
5499
+ const dataLines = event.split(/\r?\n/u).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart());
5500
+ if (dataLines.length === 0) continue;
5501
+ const data = dataLines.join("\n");
5502
+ if (data === "[DONE]") continue;
5503
+ const parsed = safeJsonParse(data);
5504
+ if (!isPlainObject$1(parsed)) continue;
5505
+ if (parsed.type === "response.output_text.delta" && typeof parsed.delta === "string") text += parsed.delta;
5506
+ if (parsed.type === "response.completed" && isPlainObject$1(parsed.response)) response = parsed.response;
5507
+ }
5508
+ return {
5509
+ ...typeof response?.id === "string" ? { id: response.id } : {},
5510
+ ...typeof response?.created_at === "number" ? { created: response.created_at } : {},
5511
+ ...typeof response?.model === "string" ? { model: response.model } : {},
5512
+ text,
5513
+ ...normalizeCodexUsage(response?.usage)
5514
+ };
5515
+ }
5516
+ function normalizeCodexUsage(usage) {
5517
+ if (!isPlainObject$1(usage)) return {};
5518
+ const promptTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : void 0;
5519
+ const completionTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : void 0;
5520
+ const totalTokens = typeof usage.total_tokens === "number" ? usage.total_tokens : void 0;
5521
+ return { usage: {
5522
+ ...promptTokens === void 0 ? {} : { prompt_tokens: promptTokens },
5523
+ ...completionTokens === void 0 ? {} : { completion_tokens: completionTokens },
5524
+ ...totalTokens === void 0 ? {} : { total_tokens: totalTokens }
5525
+ } };
5526
+ }
5527
+ async function readJsonBody(body) {
5528
+ if (typeof body !== "string") return;
5529
+ return safeJsonParse(body);
5530
+ }
5531
+ function safeJsonParse(source) {
5532
+ try {
5533
+ return JSON.parse(source);
5534
+ } catch {
5535
+ return;
5536
+ }
5537
+ }
5538
+ function isChatCompletionsBody(value) {
5539
+ return isPlainObject$1(value) && typeof value.model === "string" && Array.isArray(value.messages) && value.messages.every((message) => isPlainObject$1(message) && typeof message.role === "string");
5540
+ }
5541
+ function messageContentToText(content) {
5542
+ if (typeof content === "string") return content;
5543
+ if (!Array.isArray(content)) return "";
5544
+ return content.map((part) => {
5545
+ if (!isPlainObject$1(part)) return "";
5546
+ if (typeof part.text === "string") return part.text;
5547
+ if (typeof part.content === "string") return part.content;
5548
+ return "";
5549
+ }).filter(Boolean).join("\n");
5550
+ }
5551
+ function isPlainObject$1(value) {
5552
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5553
+ }
5363
5554
  async function resolveCodexAuth(options = {}) {
5364
5555
  const providerId = options.providerId ?? "codex";
5365
5556
  const record = (await readAuthStore({ path: options.authStorePath })).providers[providerId];
@@ -6197,7 +6388,8 @@ const openRouterProviderDefaults = {
6197
6388
  };
6198
6389
  const codexProviderDefaults = {
6199
6390
  type: "openai-compatible",
6200
- baseURL: CODEX_BACKEND_BASE_URL
6391
+ baseURL: CODEX_BACKEND_BASE_URL,
6392
+ toolProtocol: "text-json"
6201
6393
  };
6202
6394
  const codexStarterModelChoices = [
6203
6395
  "codex/gpt-5.5",
@@ -7973,11 +8165,11 @@ async function directoryExists(path) {
7973
8165
  if (!(await stat(path)).isDirectory()) throw new Error(`${path} exists but is not a folder`);
7974
8166
  return true;
7975
8167
  } catch (error) {
7976
- if (isNodeError$1(error) && error.code === "ENOENT") return false;
8168
+ if (isNodeError$2(error) && error.code === "ENOENT") return false;
7977
8169
  throw error;
7978
8170
  }
7979
8171
  }
7980
- function isNodeError$1(error) {
8172
+ function isNodeError$2(error) {
7981
8173
  return error instanceof Error && "code" in error;
7982
8174
  }
7983
8175
  //#endregion
@@ -8016,7 +8208,7 @@ async function removeIfPresent(path) {
8016
8208
  });
8017
8209
  return true;
8018
8210
  } catch (error) {
8019
- if (isNodeError(error) && error.code === "ENOENT") return false;
8211
+ if (isNodeError$1(error) && error.code === "ENOENT") return false;
8020
8212
  throw error;
8021
8213
  }
8022
8214
  }
@@ -8029,7 +8221,7 @@ function assertSafeResetPath(workspaceRoot, path) {
8029
8221
  if (target === workspace) throw new Error(`Refusing to reset KB because the configured KB path is the workspace root: ${target}`);
8030
8222
  if (dirname(target) === target) throw new Error(`Refusing to reset KB because the configured KB path is a filesystem root: ${target}`);
8031
8223
  }
8032
- function isNodeError(error) {
8224
+ function isNodeError$1(error) {
8033
8225
  return error instanceof Error && "code" in error;
8034
8226
  }
8035
8227
  //#endregion
@@ -11844,10 +12036,16 @@ const agentMetadataSchema = z.object({
11844
12036
  capabilities: z.array(z.string().min(1)).optional(),
11845
12037
  model_support: agentModelSupportSchema.optional()
11846
12038
  }).strict();
11847
- z.object({
12039
+ const agentsMetadata = z.object({
11848
12040
  version: z.literal(1),
11849
12041
  agents: z.record(z.string().min(1), agentMetadataSchema)
11850
12042
  }).strict().parse(agents_default);
12043
+ function listAgentMetadata() {
12044
+ return Object.entries(agentsMetadata.agents).map(([id, metadata]) => ({
12045
+ id,
12046
+ metadata
12047
+ })).sort((left, right) => left.id.localeCompare(right.id));
12048
+ }
11851
12049
  //#endregion
11852
12050
  //#region src/agent/prompts.ts
11853
12051
  function getChatSystemPrompt(options = {}) {
@@ -15317,6 +15515,135 @@ function formatInfoError(error) {
15317
15515
  return error instanceof Error ? error.message : String(error);
15318
15516
  }
15319
15517
  //#endregion
15518
+ //#region src/cli/integrations.ts
15519
+ const TOPCHESTER_CODEX_BLOCK_START = "# >>> topchester integration: codex";
15520
+ const TOPCHESTER_CODEX_BLOCK_END = "# <<< topchester integration: codex";
15521
+ function listIntegrationStatuses() {
15522
+ return listAgentMetadata().filter(({ id }) => id !== "topchester").map(({ id, metadata }) => getIntegrationStatus(id, metadata.display_name));
15523
+ }
15524
+ function getIntegrationStatus(agent, displayName) {
15525
+ const normalizedAgent = normalizeAgentId(agent);
15526
+ const metadata = listAgentMetadata().find(({ id }) => id === normalizedAgent)?.metadata;
15527
+ const name = displayName ?? metadata?.display_name ?? normalizedAgent;
15528
+ if (normalizedAgent !== "codex") return {
15529
+ agent: normalizedAgent,
15530
+ displayName: name,
15531
+ supported: false,
15532
+ installed: false,
15533
+ detail: "not supported yet"
15534
+ };
15535
+ return {
15536
+ agent: normalizedAgent,
15537
+ displayName: name,
15538
+ supported: true,
15539
+ installed: false,
15540
+ configPath: getCodexConfigPath(),
15541
+ detail: "not installed"
15542
+ };
15543
+ }
15544
+ async function getIntegrationStatusAsync(agent) {
15545
+ const status = getIntegrationStatus(agent);
15546
+ if (status.agent !== "codex" || !status.configPath) return status;
15547
+ const contents = await readTextIfExists(status.configPath);
15548
+ const installed = contents.includes(TOPCHESTER_CODEX_BLOCK_START) && contents.includes(TOPCHESTER_CODEX_BLOCK_END);
15549
+ return {
15550
+ ...status,
15551
+ installed,
15552
+ detail: installed ? "installed" : "not installed"
15553
+ };
15554
+ }
15555
+ async function listIntegrationStatusesAsync(agent) {
15556
+ if (agent) return [await getIntegrationStatusAsync(agent)];
15557
+ return await Promise.all(listIntegrationStatuses().map((status) => getIntegrationStatusAsync(status.agent)));
15558
+ }
15559
+ async function installIntegration(agent, action = "installed") {
15560
+ const status = await getIntegrationStatusAsync(agent);
15561
+ if (!status.supported || status.agent !== "codex" || !status.configPath) throw new Error(`Unsupported integration: ${agent}`);
15562
+ const next = appendMarkedBlock(removeMarkedBlock(await readTextIfExists(status.configPath)), formatCodexIntegrationBlock());
15563
+ await mkdir(dirname(status.configPath), { recursive: true });
15564
+ await writeFile(status.configPath, next);
15565
+ return {
15566
+ action,
15567
+ status: await getIntegrationStatusAsync(status.agent)
15568
+ };
15569
+ }
15570
+ async function removeIntegration(agent) {
15571
+ const status = await getIntegrationStatusAsync(agent);
15572
+ if (!status.supported || status.agent !== "codex" || !status.configPath) throw new Error(`Unsupported integration: ${agent}`);
15573
+ const before = await readTextIfExists(status.configPath);
15574
+ await mkdir(dirname(status.configPath), { recursive: true });
15575
+ await writeFile(status.configPath, ensureTrailingNewline(removeMarkedBlock(before)));
15576
+ return {
15577
+ action: "removed",
15578
+ status: await getIntegrationStatusAsync(status.agent)
15579
+ };
15580
+ }
15581
+ function formatIntegrationStatuses(statuses) {
15582
+ const lines = ["Integrations"];
15583
+ for (const status of statuses) {
15584
+ const state = status.supported ? status.installed ? "installed" : "not installed" : "unsupported";
15585
+ lines.push(`${status.agent}: ${state}`);
15586
+ if (status.configPath) lines.push(` config: ${status.configPath}`);
15587
+ if (!status.supported) lines.push(` detail: ${status.detail}`);
15588
+ }
15589
+ return lines;
15590
+ }
15591
+ function formatIntegrationAction(action, status) {
15592
+ return [
15593
+ action === "installed" ? "Integration installed" : action === "repaired" ? "Integration repaired" : "Integration removed",
15594
+ `agent: ${status.agent}`,
15595
+ `state: ${status.installed ? "installed" : "not installed"}`,
15596
+ ...status.configPath ? [`config: ${status.configPath}`] : []
15597
+ ];
15598
+ }
15599
+ async function executeHookStop(agent) {
15600
+ if (normalizeAgentId(agent ?? "") !== "codex") throw new Error(agent ? `Unsupported hook agent: ${agent}` : "Usage: topchester hook stop <agent>");
15601
+ }
15602
+ function getCodexConfigPath() {
15603
+ return join(process.env.CODEX_HOME?.trim() || join(process.env.HOME || homedir(), ".codex"), "config.toml");
15604
+ }
15605
+ function formatCodexIntegrationBlock() {
15606
+ return [
15607
+ TOPCHESTER_CODEX_BLOCK_START,
15608
+ "[[Stop]]",
15609
+ "",
15610
+ "[[Stop.hooks]]",
15611
+ "type = \"command\"",
15612
+ "command = \"topchester hook stop codex\"",
15613
+ "timeout = 10",
15614
+ "statusMessage = \"Notifying Topchester\"",
15615
+ TOPCHESTER_CODEX_BLOCK_END
15616
+ ].join("\n");
15617
+ }
15618
+ function appendMarkedBlock(contents, block) {
15619
+ const base = contents.trimEnd();
15620
+ return `${base ? `${base}\n\n` : ""}${block}\n`;
15621
+ }
15622
+ function removeMarkedBlock(contents) {
15623
+ const start = contents.indexOf(TOPCHESTER_CODEX_BLOCK_START);
15624
+ if (start === -1) return contents;
15625
+ const end = contents.indexOf(TOPCHESTER_CODEX_BLOCK_END, start);
15626
+ if (end === -1) return contents;
15627
+ return `${contents.slice(0, start)}${contents.slice(end + 35)}`.trimEnd();
15628
+ }
15629
+ async function readTextIfExists(path) {
15630
+ try {
15631
+ return await readFile(path, "utf8");
15632
+ } catch (error) {
15633
+ if (isNodeError(error) && error.code === "ENOENT") return "";
15634
+ throw error;
15635
+ }
15636
+ }
15637
+ function ensureTrailingNewline(value) {
15638
+ return value ? `${value.trimEnd()}\n` : "";
15639
+ }
15640
+ function normalizeAgentId(value) {
15641
+ return value.trim().toLowerCase();
15642
+ }
15643
+ function isNodeError(error) {
15644
+ return error instanceof Error && "code" in error;
15645
+ }
15646
+ //#endregion
15320
15647
  //#region src/cli.ts
15321
15648
  async function runTopchesterCli(argv = process.argv, options = {}) {
15322
15649
  const program = createTopchesterProgram();
@@ -15407,6 +15734,58 @@ function createTopchesterProgram() {
15407
15734
  program.command("search").description("search compiled L1 knowledge entries").argument("<query...>", "search query").option("--limit <count>", "maximum number of matches", parsePositiveInteger).option("--json", "write full JSON search result to stdout").action(async (queryParts, options) => {
15408
15735
  await executeKbSearchCommand(program, queryParts, options);
15409
15736
  });
15737
+ const integrationsCommand = program.command("integrations").description("manage Topchester integrations with other agents");
15738
+ integrationsCommand.command("list").description("list supported integrations").action(async () => {
15739
+ try {
15740
+ console.log(formatIntegrationStatuses(await listIntegrationStatusesAsync()).join("\n"));
15741
+ } catch (error) {
15742
+ console.error(formatStartupError(error));
15743
+ process.exitCode = 1;
15744
+ }
15745
+ });
15746
+ integrationsCommand.command("status").description("check whether integrations are installed").argument("[agent]", "agent id").action(async (agent) => {
15747
+ try {
15748
+ console.log(formatIntegrationStatuses(await listIntegrationStatusesAsync(agent)).join("\n"));
15749
+ } catch (error) {
15750
+ console.error(formatStartupError(error));
15751
+ process.exitCode = 1;
15752
+ }
15753
+ });
15754
+ integrationsCommand.command("install").description("install Topchester lifecycle hooks for another agent").argument("<agent>", "agent id").action(async (agent) => {
15755
+ try {
15756
+ const result = await installIntegration(agent);
15757
+ console.log(formatIntegrationAction(result.action, result.status).join("\n"));
15758
+ } catch (error) {
15759
+ console.error(formatStartupError(error));
15760
+ process.exitCode = 1;
15761
+ }
15762
+ });
15763
+ integrationsCommand.command("repair").description("repair a Topchester integration").argument("<agent>", "agent id").action(async (agent) => {
15764
+ try {
15765
+ const result = await installIntegration(agent, "repaired");
15766
+ console.log(formatIntegrationAction(result.action, result.status).join("\n"));
15767
+ } catch (error) {
15768
+ console.error(formatStartupError(error));
15769
+ process.exitCode = 1;
15770
+ }
15771
+ });
15772
+ integrationsCommand.command("remove").description("remove a Topchester integration").argument("<agent>", "agent id").action(async (agent) => {
15773
+ try {
15774
+ const result = await removeIntegration(agent);
15775
+ console.log(formatIntegrationAction(result.action, result.status).join("\n"));
15776
+ } catch (error) {
15777
+ console.error(formatStartupError(error));
15778
+ process.exitCode = 1;
15779
+ }
15780
+ });
15781
+ program.command("hook").description("low-level lifecycle hook endpoints").command("stop").description("receive a Stop hook from another agent").argument("<agent>", "agent id").action(async (agent) => {
15782
+ try {
15783
+ await executeHookStop(agent);
15784
+ } catch (error) {
15785
+ console.error(formatStartupError(error));
15786
+ process.exitCode = 1;
15787
+ }
15788
+ });
15410
15789
  const kbCommand = program.command("kb").description("knowledge base commands");
15411
15790
  kbCommand.command("init").description("initialize a project knowledge base").action(async () => {
15412
15791
  const context = createContextFromOptions(program);
@@ -15650,4 +16029,4 @@ function formatDryRunSyncStatus(status) {
15650
16029
  //#endregion
15651
16030
  export { runTopchesterCli as t };
15652
16031
 
15653
- //# sourceMappingURL=cli-CqFhOvlt.mjs.map
16032
+ //# sourceMappingURL=cli-AJknxj59.mjs.map