@longtable/cli 0.1.32 → 0.1.33

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/cli.js CHANGED
@@ -6,6 +6,7 @@ import { createInterface } from "node:readline/promises";
6
6
  import { stdin as input, stdout as output, cwd, env, exit } from "node:process";
7
7
  import { dirname, join, resolve } from "node:path";
8
8
  import { homedir } from "node:os";
9
+ import { fileURLToPath } from "node:url";
9
10
  import { classifyCheckpointTrigger } from "@longtable/checkpoints";
10
11
  import { assessSearchSourceCapabilities, buildResearchSearchIntent, buildSearchCapabilitySnapshot, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, searchCapabilitySnapshotPath, summarizeConfiguredPublisherAccess } from "./search/index.js";
11
12
  import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupOutput, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
@@ -15,7 +16,8 @@ import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodex
15
16
  import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router.js";
16
17
  import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
17
18
  import { buildPanelFallback, renderPanelSummary } from "./panel.js";
18
- import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
19
+ import { LONGTABLE_MANAGED_HOOK_EVENTS, codexHooksEnabled, enableCodexHooksFeature, getMissingManagedCodexHookEvents, mergeManagedCodexHooksConfig, removeManagedCodexHooks } from "./codex-hooks.js";
20
+ import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, clearWorkspaceQuestion, createWorkspaceFollowUpQuestions, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadWorkspaceState, loadProjectContextFromDirectory, repairWorkspaceStateConsistency, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
19
21
  import { buildTeamDebate, buildTeamReview, renderTeamDebateSummary } from "./debate.js";
20
22
  import { createPromptRenderer } from "./prompt-renderer.js";
21
23
  const VALID_MODES = new Set([
@@ -43,7 +45,7 @@ const ANSI = {
43
45
  green: "\u001B[32m"
44
46
  };
45
47
  const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
46
- const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.32";
48
+ const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.33";
47
49
  const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
48
50
  const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
49
51
  function style(text, prefix) {
@@ -97,8 +99,8 @@ function usage() {
97
99
  " longtable init [deprecated alias for setup; full legacy flags still supported for automation]",
98
100
  " longtable start [deprecated fallback] [--path <dir>] [--name <project>] [--goal <text>] [--blocker <text>] [--research-object research_question|theory_framework|measurement_instrument|study_design|analysis_plan|manuscript] [--gap-risk known_gap|suspected_tacit_assumptions|diagnose] [--protected-decision theory|measurement|method|evidence_citation|authorship_voice|submission_public_sharing] [--perspectives <role[,role]>] [--disagreement synthesis_only|show_on_conflict|always_visible] [--setup <path>] [--json] [--no-interview]",
99
101
  " longtable resume [--cwd <path>] [--json]",
100
- " longtable doctor [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
101
- " longtable status [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
102
+ " longtable doctor [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--hooks-path <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
103
+ " longtable status [--cwd <path>] [--fix] [--json] [--codex-dir <path>] [--codex-config <path>] [--hooks-path <path>] [--claude-dir <path>] [--codex-prompts-dir <path>] [--codex-runtime-path <file>] [--claude-runtime-path <file>]",
102
104
  " longtable roles [--json]",
103
105
  " longtable show [--json] [--path <file>]",
104
106
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
@@ -112,6 +114,7 @@ function usage() {
112
114
  " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
113
115
  " longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
114
116
  " longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
117
+ " longtable clear-question --question <id> --reason <text> [--cwd <path>] [--json]",
115
118
  " longtable panel [--prompt <text>] [--role <role[,role]>] [--mode review|critique|draft|commit] [--visibility synthesis_only|show_on_conflict|always_visible] [--print] [--json] [--setup <path>] [--cwd <path>]",
116
119
  " longtable decide [--question <id>] --answer <value-or-text> [--rationale <text>] [--provider codex|claude] [--cwd <path>] [--json]",
117
120
  " longtable explore|review|critique|draft|commit|submit [--prompt <text>] [--role <role[,role]>] [--panel] [--show-conflicts] [--show-deliberation] [--print] [--json] [--stage <stage>] [--setup <path>] [--cwd <path>]",
@@ -120,7 +123,9 @@ function usage() {
120
123
  " longtable codex remove-skills [--dir <path>]",
121
124
  " longtable codex install-prompts [--dir <path>]",
122
125
  " longtable codex remove-prompts [--dir <path>]",
123
- " longtable codex status [--dir <path>] [--json]",
126
+ " longtable codex install-hooks [--codex-config <path>] [--hooks-path <path>] [--json]",
127
+ " longtable codex remove-hooks [--codex-config <path>] [--hooks-path <path>] [--json]",
128
+ " longtable codex status [--dir <path>] [--codex-config <path>] [--hooks-path <path>] [--json]",
124
129
  " longtable claude install-skills [--dir <path>]",
125
130
  " longtable claude remove-skills [--dir <path>]",
126
131
  " longtable claude status [--dir <path>] [--json]",
@@ -144,7 +149,7 @@ function parseArgs(argv) {
144
149
  const values = {};
145
150
  let subcommand = maybeSubcommand;
146
151
  const modeCommand = command && VALID_MODES.has(command);
147
- const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "panel", "decide", "sentinel", "team", "search"].includes(command);
152
+ const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "panel", "decide", "sentinel", "team", "search"].includes(command);
148
153
  let startIndex = 1;
149
154
  if (modeCommand) {
150
155
  subcommand = undefined;
@@ -1113,6 +1118,16 @@ function resolveCodexMcpConfigPath(args) {
1113
1118
  ? args["codex-config"].trim()
1114
1119
  : "~/.codex/config.toml"));
1115
1120
  }
1121
+ function resolveCodexHooksPath(args) {
1122
+ if (typeof args["hooks-path"] === "string" && args["hooks-path"].trim()) {
1123
+ return resolve(normalizeUserPath(args["hooks-path"]));
1124
+ }
1125
+ const configPath = resolveCodexMcpConfigPath(args);
1126
+ return resolve(dirname(configPath), "hooks.json");
1127
+ }
1128
+ function resolveCliPackageRoot() {
1129
+ return resolve(fileURLToPath(new URL("..", import.meta.url)));
1130
+ }
1116
1131
  function resolveClaudeMcpSettingsPath(args) {
1117
1132
  return resolve(normalizeUserPath(typeof args["claude-settings"] === "string" && args["claude-settings"].trim()
1118
1133
  ? args["claude-settings"].trim()
@@ -1195,6 +1210,56 @@ async function writeClaudeMcpSettings(path, serverName, command, mcpArgs) {
1195
1210
  await writeFile(path, `${updated}\n`, "utf8");
1196
1211
  return `${updated}\n`;
1197
1212
  }
1213
+ async function installCodexNativeHooks(args) {
1214
+ const configPath = resolveCodexMcpConfigPath(args);
1215
+ const hooksPath = resolveCodexHooksPath(args);
1216
+ const packageRoot = resolveCliPackageRoot();
1217
+ const existingConfig = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
1218
+ const existingHooks = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
1219
+ const nextConfig = enableCodexHooksFeature(existingConfig);
1220
+ const nextHooks = mergeManagedCodexHooksConfig(existingHooks, packageRoot);
1221
+ await mkdir(dirname(configPath), { recursive: true });
1222
+ await mkdir(dirname(hooksPath), { recursive: true });
1223
+ await writeFile(configPath, nextConfig, "utf8");
1224
+ await writeFile(hooksPath, nextHooks, "utf8");
1225
+ return {
1226
+ configPath,
1227
+ hooksPath,
1228
+ codexHooksEnabled: codexHooksEnabled(nextConfig),
1229
+ managedEvents: [...LONGTABLE_MANAGED_HOOK_EVENTS],
1230
+ write: true
1231
+ };
1232
+ }
1233
+ async function removeCodexNativeHooks(args) {
1234
+ const configPath = resolveCodexMcpConfigPath(args);
1235
+ const hooksPath = resolveCodexHooksPath(args);
1236
+ const existingHooks = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
1237
+ const removed = existingHooks ? removeManagedCodexHooks(existingHooks) : { nextContent: null, removedCount: 0 };
1238
+ if (removed.nextContent === null) {
1239
+ await rm(hooksPath, { force: true });
1240
+ }
1241
+ else {
1242
+ await mkdir(dirname(hooksPath), { recursive: true });
1243
+ await writeFile(hooksPath, removed.nextContent, "utf8");
1244
+ }
1245
+ const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
1246
+ return {
1247
+ configPath,
1248
+ hooksPath,
1249
+ codexHooksEnabled: codexHooksEnabled(configContent),
1250
+ managedEvents: removed.removedCount > 0 ? [...LONGTABLE_MANAGED_HOOK_EVENTS] : [],
1251
+ write: true
1252
+ };
1253
+ }
1254
+ function renderCodexHookInstallSummary(result) {
1255
+ return [
1256
+ "LongTable Codex hooks",
1257
+ `- config: ${result.configPath}`,
1258
+ `- hooks: ${result.hooksPath}`,
1259
+ `- codex_hooks feature: ${result.codexHooksEnabled ? "enabled" : "missing"}`,
1260
+ `- managed events: ${result.managedEvents.length > 0 ? result.managedEvents.join(", ") : "none"}`
1261
+ ].join("\n");
1262
+ }
1198
1263
  function renderMcpInstallSummary(result) {
1199
1264
  const lines = [
1200
1265
  "LongTable MCP transport",
@@ -1381,6 +1446,13 @@ async function collectDoctorStatus(args) {
1381
1446
  const codexMcpConfig = existsSync(codexMcpConfigPath)
1382
1447
  ? await readFile(codexMcpConfigPath, "utf8")
1383
1448
  : "";
1449
+ const codexHooksPath = resolveCodexHooksPath(args);
1450
+ const codexHooksContent = existsSync(codexHooksPath)
1451
+ ? await readFile(codexHooksPath, "utf8")
1452
+ : "";
1453
+ const missingManagedHookEvents = codexHooksContent
1454
+ ? (getMissingManagedCodexHookEvents(codexHooksContent) ?? [...LONGTABLE_MANAGED_HOOK_EVENTS])
1455
+ : [...LONGTABLE_MANAGED_HOOK_EVENTS];
1384
1456
  const expectedCodexSkills = buildCodexSkillSpecs(roles).map((skill) => skill.name);
1385
1457
  const expectedClaudeSkills = buildClaudeSkillSpecs(roles).map((skill) => skill.name);
1386
1458
  const [codexSkills, claudeSkills, codexAliases, workspace] = await Promise.all([
@@ -1409,7 +1481,11 @@ async function collectDoctorStatus(args) {
1409
1481
  mcpConfigPath: codexMcpConfigPath,
1410
1482
  mcpConfigExists: existsSync(codexMcpConfigPath),
1411
1483
  longtableMcpConfigured: codexLongTableMcpConfigured(codexMcpConfig),
1412
- mcpElicitationsAllowed: codexMcpElicitationsAllowed(codexMcpConfig)
1484
+ mcpElicitationsAllowed: codexMcpElicitationsAllowed(codexMcpConfig),
1485
+ hooksPath: codexHooksPath,
1486
+ hooksExists: existsSync(codexHooksPath),
1487
+ codexHooksEnabled: codexHooksEnabled(codexMcpConfig),
1488
+ missingManagedHookEvents
1413
1489
  },
1414
1490
  claude: {
1415
1491
  command: "claude",
@@ -1451,6 +1527,9 @@ function renderDoctorStatus(status) {
1451
1527
  `- MCP config: ${status.providers.codex.mcpConfigExists ? "present" : "missing"} (${status.providers.codex.mcpConfigPath})`,
1452
1528
  `- LongTable MCP: ${status.providers.codex.longtableMcpConfigured ? "configured" : "missing"}`,
1453
1529
  `- MCP elicitation approval: ${status.providers.codex.mcpElicitationsAllowed ? "allowed" : "not allowed"}`,
1530
+ `- Codex hooks file: ${status.providers.codex.hooksExists ? "present" : "missing"} (${status.providers.codex.hooksPath})`,
1531
+ `- codex_hooks feature: ${status.providers.codex.codexHooksEnabled ? "enabled" : "missing"}`,
1532
+ `- managed hook coverage: ${status.providers.codex.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.providers.codex.missingManagedHookEvents.join(", ")}`}`,
1454
1533
  "",
1455
1534
  ...renderProviderDoctorBlock("Claude", status.providers.claude),
1456
1535
  "",
@@ -1461,7 +1540,7 @@ function renderDoctorStatus(status) {
1461
1540
  }
1462
1541
  else {
1463
1542
  const workspace = status.workspace;
1464
- lines.push(`- project: ${workspace.project?.name ?? "unknown"}`, `- root: ${workspace.rootPath ?? "unknown"}`, `- goal: ${workspace.session?.currentGoal ?? "unknown"}`, `- invocations: ${workspace.counts?.invocations ?? 0}`, `- questions: ${workspace.counts?.questions ?? 0} (${workspace.counts?.pendingQuestions ?? 0} pending, ${workspace.counts?.answeredQuestions ?? 0} answered)`, `- decisions: ${workspace.counts?.decisions ?? 0}`);
1543
+ lines.push(`- project: ${workspace.project?.name ?? "unknown"}`, `- root: ${workspace.rootPath ?? "unknown"}`, `- goal: ${workspace.session?.currentGoal ?? "unknown"}`, `- invocations: ${workspace.counts?.invocations ?? 0}`, `- questions: ${workspace.counts?.questions ?? 0} (${workspace.counts?.pendingQuestions ?? 0} pending, ${workspace.counts?.answeredQuestions ?? 0} answered)`, `- obligations: ${workspace.counts?.pendingObligations ?? 0} pending`, `- decisions: ${workspace.counts?.decisions ?? 0}`);
1465
1544
  if ((workspace.recentInvocations ?? []).length > 0) {
1466
1545
  lines.push("- recent invocations:");
1467
1546
  for (const invocation of workspace.recentInvocations ?? []) {
@@ -1475,6 +1554,12 @@ function renderDoctorStatus(status) {
1475
1554
  lines.push(` - ${question.id}: ${question.question} (${question.options.join("/")})`);
1476
1555
  }
1477
1556
  }
1557
+ if ((workspace.pendingObligations ?? []).length > 0) {
1558
+ lines.push("- pending obligations:");
1559
+ for (const obligation of workspace.pendingObligations ?? []) {
1560
+ lines.push(` - ${obligation.id}: ${obligation.prompt}`);
1561
+ }
1562
+ }
1478
1563
  if ((workspace.answerWarnings ?? []).length > 0) {
1479
1564
  lines.push("- answer warnings:");
1480
1565
  for (const warning of workspace.answerWarnings ?? []) {
@@ -1489,11 +1574,16 @@ function renderDoctorStatus(status) {
1489
1574
  const canFix = status.providers.codex.missingSkills.length > 0 ||
1490
1575
  status.providers.claude.missingSkills.length > 0 ||
1491
1576
  status.providers.codex.legacyPromptFilesInstalled.length > 0 ||
1577
+ !status.providers.codex.codexHooksEnabled ||
1578
+ status.providers.codex.missingManagedHookEvents.length > 0 ||
1492
1579
  (status.setupExists &&
1493
1580
  (!status.providers.codex.runtimeExists || !status.providers.claude.runtimeExists));
1494
1581
  if (canFix) {
1495
1582
  nextActions.push("longtable doctor --fix");
1496
1583
  }
1584
+ if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
1585
+ nextActions.push("longtable codex install-hooks");
1586
+ }
1497
1587
  if (!status.setupExists) {
1498
1588
  nextActions.push("longtable setup --provider codex");
1499
1589
  }
@@ -1523,6 +1613,15 @@ function renderRepairSummary(repair) {
1523
1613
  if (repair.removedLegacyPromptFiles.length > 0) {
1524
1614
  lines.push(`- removed legacy prompt files: ${repair.removedLegacyPromptFiles.length}`);
1525
1615
  }
1616
+ if (repair.installedCodexHooks) {
1617
+ lines.push("- installed Codex native hooks");
1618
+ }
1619
+ if ((repair.repairedWorkspaceState ?? []).length > 0) {
1620
+ lines.push("- repaired workspace state:");
1621
+ for (const item of repair.repairedWorkspaceState ?? []) {
1622
+ lines.push(` - ${item}`);
1623
+ }
1624
+ }
1526
1625
  if (repair.writtenRuntimeConfigs.length > 0) {
1527
1626
  lines.push("- wrote runtime configs:");
1528
1627
  for (const target of repair.writtenRuntimeConfigs) {
@@ -1574,6 +1673,8 @@ async function repairDoctorStatus(args, status) {
1574
1673
  installedCodexSkills: [],
1575
1674
  installedClaudeSkills: [],
1576
1675
  removedLegacyPromptFiles: [],
1676
+ installedCodexHooks: false,
1677
+ repairedWorkspaceState: [],
1577
1678
  writtenRuntimeConfigs: [],
1578
1679
  skipped: []
1579
1680
  };
@@ -1586,8 +1687,16 @@ async function repairDoctorStatus(args, status) {
1586
1687
  if (status.providers.codex.legacyPromptFilesInstalled.length > 0) {
1587
1688
  repair.removedLegacyPromptFiles = await removeCodexPromptAliases(codexPromptsDir);
1588
1689
  }
1690
+ if (!status.providers.codex.codexHooksEnabled || status.providers.codex.missingManagedHookEvents.length > 0) {
1691
+ await installCodexNativeHooks(args);
1692
+ repair.installedCodexHooks = true;
1693
+ }
1589
1694
  if (!status.setupExists) {
1590
1695
  repair.skipped.push("runtime configs require setup approval; run `longtable setup --provider codex` first");
1696
+ const workspaceContext = await loadProjectContextFromDirectory(typeof args.cwd === "string" ? args.cwd : cwd());
1697
+ if (workspaceContext) {
1698
+ repair.repairedWorkspaceState = (await repairWorkspaceStateConsistency({ context: workspaceContext })).repaired;
1699
+ }
1591
1700
  return repair;
1592
1701
  }
1593
1702
  const setup = await loadSetupOutput(setupOverride);
@@ -1607,6 +1716,10 @@ async function repairDoctorStatus(args, status) {
1607
1716
  format: target.format
1608
1717
  });
1609
1718
  }
1719
+ const workspaceContext = await loadProjectContextFromDirectory(typeof args.cwd === "string" ? args.cwd : cwd());
1720
+ if (workspaceContext) {
1721
+ repair.repairedWorkspaceState = (await repairWorkspaceStateConsistency({ context: workspaceContext })).repaired;
1722
+ }
1610
1723
  return repair;
1611
1724
  }
1612
1725
  async function runDoctor(args) {
@@ -2331,6 +2444,41 @@ async function runQuestion(args) {
2331
2444
  console.log(`- answer: longtable decide --question ${result.question.id} --answer <value>`);
2332
2445
  console.log(`- current: ${context.currentFilePath}`);
2333
2446
  }
2447
+ async function runClearQuestion(args) {
2448
+ const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
2449
+ const questionId = typeof args.question === "string" ? args.question.trim() : "";
2450
+ const reason = typeof args.reason === "string" ? args.reason.trim() : "";
2451
+ if (!questionId) {
2452
+ throw new Error("`clear-question` requires --question <id>.");
2453
+ }
2454
+ if (!reason) {
2455
+ throw new Error("`clear-question` requires --reason <text>.");
2456
+ }
2457
+ const context = await loadProjectContextFromDirectory(workingDirectory);
2458
+ if (!context) {
2459
+ throw new Error("No LongTable project workspace was found here. Run this inside a project or pass --cwd.");
2460
+ }
2461
+ const result = await clearWorkspaceQuestion({
2462
+ context,
2463
+ questionId,
2464
+ reason
2465
+ });
2466
+ if (args.json === true) {
2467
+ console.log(JSON.stringify({
2468
+ question: result.question,
2469
+ files: {
2470
+ state: context.stateFilePath,
2471
+ current: context.currentFilePath
2472
+ }
2473
+ }, null, 2));
2474
+ return;
2475
+ }
2476
+ console.log("LongTable question cleared");
2477
+ console.log(`- question: ${result.question.id}`);
2478
+ console.log(`- reason: ${result.question.clearedReason ?? reason}`);
2479
+ console.log(`- state: ${context.stateFilePath}`);
2480
+ console.log(`- current: ${context.currentFilePath}`);
2481
+ }
2334
2482
  function isInteractiveTerminal() {
2335
2483
  return Boolean(input.isTTY && output.isTTY);
2336
2484
  }
@@ -2933,11 +3081,34 @@ async function runCodexSubcommand(subcommand, args) {
2933
3081
  console.log(`Removed ${removed.length} legacy LongTable prompt files from ${resolveCodexPromptsDir(customDir)}`);
2934
3082
  return;
2935
3083
  }
3084
+ if (subcommand === "install-hooks") {
3085
+ const result = await installCodexNativeHooks(args);
3086
+ if (args.json === true) {
3087
+ console.log(JSON.stringify(result, null, 2));
3088
+ return;
3089
+ }
3090
+ console.log(renderCodexHookInstallSummary(result));
3091
+ console.log("Restart Codex so the native hook config is reloaded.");
3092
+ return;
3093
+ }
3094
+ if (subcommand === "remove-hooks") {
3095
+ const result = await removeCodexNativeHooks(args);
3096
+ if (args.json === true) {
3097
+ console.log(JSON.stringify(result, null, 2));
3098
+ return;
3099
+ }
3100
+ console.log(renderCodexHookInstallSummary(result));
3101
+ return;
3102
+ }
2936
3103
  if (subcommand === "status") {
2937
3104
  const aliases = await listInstalledCodexPromptAliases(customDir);
2938
3105
  const skills = await listInstalledCodexSkills(roles, customDir);
2939
3106
  const setupPath = resolveDefaultSetupPath(typeof args.path === "string" ? args.path : undefined).path;
2940
3107
  const runtimePath = resolveDefaultRuntimeConfigPath("codex", typeof args["runtime-path"] === "string" ? args["runtime-path"] : undefined).path;
3108
+ const configPath = resolveCodexMcpConfigPath(args);
3109
+ const configContent = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
3110
+ const hooksPath = resolveCodexHooksPath(args);
3111
+ const hooksContent = existsSync(hooksPath) ? await readFile(hooksPath, "utf8") : "";
2941
3112
  const status = {
2942
3113
  setupPath,
2943
3114
  setupExists: existsSync(setupPath),
@@ -2946,7 +3117,14 @@ async function runCodexSubcommand(subcommand, args) {
2946
3117
  skillsDir: resolveCodexSkillsDir(customDir),
2947
3118
  skillsInstalled: skills.map((skill) => skill.name),
2948
3119
  promptsDir: resolveCodexPromptsDir(customDir),
2949
- legacyPromptFilesInstalled: aliases.map((alias) => alias.name)
3120
+ legacyPromptFilesInstalled: aliases.map((alias) => alias.name),
3121
+ codexConfigPath: configPath,
3122
+ codexHooksEnabled: codexHooksEnabled(configContent),
3123
+ hooksPath,
3124
+ hooksExists: existsSync(hooksPath),
3125
+ missingManagedHookEvents: hooksContent
3126
+ ? (getMissingManagedCodexHookEvents(hooksContent) ?? [...LONGTABLE_MANAGED_HOOK_EVENTS])
3127
+ : [...LONGTABLE_MANAGED_HOOK_EVENTS]
2950
3128
  };
2951
3129
  if (args.json === true) {
2952
3130
  console.log(JSON.stringify(status, null, 2));
@@ -2976,6 +3154,10 @@ async function runCodexSubcommand(subcommand, args) {
2976
3154
  console.log(` - ${alias.name}`);
2977
3155
  }
2978
3156
  }
3157
+ console.log(`- codex config: ${status.codexConfigPath}`);
3158
+ console.log(`- codex_hooks feature: ${status.codexHooksEnabled ? "enabled" : "missing"}`);
3159
+ console.log(`- hooks file: ${status.hooksExists ? "present" : "missing"} (${status.hooksPath})`);
3160
+ console.log(`- managed hook coverage: ${status.missingManagedHookEvents.length === 0 ? "complete" : `missing ${status.missingManagedHookEvents.join(", ")}`}`);
2979
3161
  return;
2980
3162
  }
2981
3163
  throw new Error("Unknown codex subcommand.");
@@ -3089,6 +3271,10 @@ async function main() {
3089
3271
  await runQuestion(values);
3090
3272
  return;
3091
3273
  }
3274
+ if (command === "clear-question") {
3275
+ await runClearQuestion(values);
3276
+ return;
3277
+ }
3092
3278
  if (command === "panel") {
3093
3279
  await runPanelCommand(values);
3094
3280
  return;
@@ -0,0 +1,22 @@
1
+ export declare const LONGTABLE_MANAGED_HOOK_EVENTS: readonly ["SessionStart", "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"];
2
+ type ManagedHookEventName = (typeof LONGTABLE_MANAGED_HOOK_EVENTS)[number];
3
+ type JsonObject = Record<string, unknown>;
4
+ export interface ManagedCodexHooksConfig {
5
+ hooks: Record<ManagedHookEventName, Array<Record<string, unknown>>>;
6
+ }
7
+ interface ParsedCodexHooksConfig {
8
+ root: JsonObject;
9
+ hooks: JsonObject;
10
+ }
11
+ export interface RemoveManagedCodexHooksResult {
12
+ nextContent: string | null;
13
+ removedCount: number;
14
+ }
15
+ export declare function buildManagedCodexHooksConfig(packageRoot: string): ManagedCodexHooksConfig;
16
+ export declare function parseCodexHooksConfig(content: string): ParsedCodexHooksConfig | null;
17
+ export declare function getMissingManagedCodexHookEvents(content: string): ManagedHookEventName[] | null;
18
+ export declare function mergeManagedCodexHooksConfig(existingContent: string | null | undefined, packageRoot: string): string;
19
+ export declare function removeManagedCodexHooks(existingContent: string): RemoveManagedCodexHooksResult;
20
+ export declare function enableCodexHooksFeature(existing: string): string;
21
+ export declare function codexHooksEnabled(config: string): boolean;
22
+ export {};
@@ -0,0 +1,240 @@
1
+ import { join } from "node:path";
2
+ export const LONGTABLE_MANAGED_HOOK_EVENTS = [
3
+ "SessionStart",
4
+ "PreToolUse",
5
+ "PostToolUse",
6
+ "UserPromptSubmit",
7
+ "Stop"
8
+ ];
9
+ function isPlainObject(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function cloneJson(value) {
13
+ return structuredClone(value);
14
+ }
15
+ function buildCommandHook(command, options = {}) {
16
+ const hook = {
17
+ type: "command",
18
+ command,
19
+ ...(options.statusMessage ? { statusMessage: options.statusMessage } : {}),
20
+ ...(typeof options.timeout === "number" ? { timeout: options.timeout } : {})
21
+ };
22
+ return {
23
+ ...(options.matcher ? { matcher: options.matcher } : {}),
24
+ hooks: [hook]
25
+ };
26
+ }
27
+ export function buildManagedCodexHooksConfig(packageRoot) {
28
+ const hookScript = join(packageRoot, "dist", "longtable-codex-native-hook.js");
29
+ const command = `node "${hookScript}"`;
30
+ return {
31
+ hooks: {
32
+ SessionStart: [
33
+ buildCommandHook(command, {
34
+ matcher: "startup|resume"
35
+ })
36
+ ],
37
+ PreToolUse: [
38
+ buildCommandHook(command, {
39
+ matcher: "Bash",
40
+ statusMessage: "Running LongTable checkpoint guard"
41
+ })
42
+ ],
43
+ PostToolUse: [
44
+ buildCommandHook(command, {
45
+ matcher: "Bash",
46
+ statusMessage: "Reviewing LongTable post-tool state"
47
+ })
48
+ ],
49
+ UserPromptSubmit: [
50
+ buildCommandHook(command, {
51
+ statusMessage: "Applying LongTable research context"
52
+ })
53
+ ],
54
+ Stop: [
55
+ buildCommandHook(command, {
56
+ timeout: 30
57
+ })
58
+ ]
59
+ }
60
+ };
61
+ }
62
+ export function parseCodexHooksConfig(content) {
63
+ try {
64
+ const parsed = JSON.parse(content);
65
+ if (!isPlainObject(parsed)) {
66
+ return null;
67
+ }
68
+ return {
69
+ root: cloneJson(parsed),
70
+ hooks: isPlainObject(parsed.hooks) ? cloneJson(parsed.hooks) : {}
71
+ };
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ function isLongTableManagedHookCommand(command) {
78
+ return /(?:^|[\\/])longtable-codex-native-hook\.js(?:["'\s]|$)/.test(command);
79
+ }
80
+ function countManagedHooksInEntry(entry) {
81
+ if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) {
82
+ return 0;
83
+ }
84
+ return entry.hooks.filter((hook) => isPlainObject(hook) &&
85
+ hook.type === "command" &&
86
+ typeof hook.command === "string" &&
87
+ isLongTableManagedHookCommand(hook.command)).length;
88
+ }
89
+ export function getMissingManagedCodexHookEvents(content) {
90
+ const parsed = parseCodexHooksConfig(content);
91
+ if (!parsed) {
92
+ return null;
93
+ }
94
+ return LONGTABLE_MANAGED_HOOK_EVENTS.filter((eventName) => {
95
+ const entries = Array.isArray(parsed.hooks[eventName]) ? parsed.hooks[eventName] : [];
96
+ return !entries.some((entry) => countManagedHooksInEntry(entry) > 0);
97
+ });
98
+ }
99
+ function stripManagedHooksFromEntry(entry) {
100
+ if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) {
101
+ return { entry: cloneJson(entry), removedCount: 0 };
102
+ }
103
+ const nextHooks = entry.hooks.filter((hook) => {
104
+ if (!isPlainObject(hook)) {
105
+ return true;
106
+ }
107
+ return !(hook.type === "command" &&
108
+ typeof hook.command === "string" &&
109
+ isLongTableManagedHookCommand(hook.command));
110
+ });
111
+ const removedCount = entry.hooks.length - nextHooks.length;
112
+ if (removedCount === 0) {
113
+ return { entry: cloneJson(entry), removedCount: 0 };
114
+ }
115
+ if (nextHooks.length === 0) {
116
+ return { entry: null, removedCount };
117
+ }
118
+ return {
119
+ entry: {
120
+ ...cloneJson(entry),
121
+ hooks: nextHooks
122
+ },
123
+ removedCount
124
+ };
125
+ }
126
+ function serializeCodexHooksConfig(root) {
127
+ return JSON.stringify(root, null, 2) + "\n";
128
+ }
129
+ export function mergeManagedCodexHooksConfig(existingContent, packageRoot) {
130
+ const managedConfig = buildManagedCodexHooksConfig(packageRoot);
131
+ const parsed = typeof existingContent === "string"
132
+ ? parseCodexHooksConfig(existingContent)
133
+ : null;
134
+ const nextRoot = parsed ? cloneJson(parsed.root) : {};
135
+ const nextHooks = parsed ? cloneJson(parsed.hooks) : {};
136
+ for (const eventName of LONGTABLE_MANAGED_HOOK_EVENTS) {
137
+ const existingEntries = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : [];
138
+ const preservedEntries = [];
139
+ for (const entry of existingEntries) {
140
+ const stripped = stripManagedHooksFromEntry(entry);
141
+ if (stripped.entry !== null) {
142
+ preservedEntries.push(stripped.entry);
143
+ }
144
+ }
145
+ nextHooks[eventName] = [
146
+ ...preservedEntries,
147
+ ...managedConfig.hooks[eventName].map((entry) => cloneJson(entry))
148
+ ];
149
+ }
150
+ nextRoot.hooks = nextHooks;
151
+ return serializeCodexHooksConfig(nextRoot);
152
+ }
153
+ export function removeManagedCodexHooks(existingContent) {
154
+ const parsed = parseCodexHooksConfig(existingContent);
155
+ if (!parsed) {
156
+ return { nextContent: existingContent, removedCount: 0 };
157
+ }
158
+ const nextRoot = cloneJson(parsed.root);
159
+ const nextHooks = cloneJson(parsed.hooks);
160
+ let removedCount = 0;
161
+ for (const [eventName, rawEntries] of Object.entries(nextHooks)) {
162
+ if (!Array.isArray(rawEntries)) {
163
+ continue;
164
+ }
165
+ const preservedEntries = [];
166
+ for (const entry of rawEntries) {
167
+ const stripped = stripManagedHooksFromEntry(entry);
168
+ removedCount += stripped.removedCount;
169
+ if (stripped.entry !== null) {
170
+ preservedEntries.push(stripped.entry);
171
+ }
172
+ }
173
+ if (preservedEntries.length > 0) {
174
+ nextHooks[eventName] = preservedEntries;
175
+ }
176
+ else {
177
+ delete nextHooks[eventName];
178
+ }
179
+ }
180
+ if (removedCount === 0) {
181
+ return { nextContent: existingContent, removedCount: 0 };
182
+ }
183
+ if (Object.keys(nextHooks).length > 0) {
184
+ nextRoot.hooks = nextHooks;
185
+ return { nextContent: serializeCodexHooksConfig(nextRoot), removedCount };
186
+ }
187
+ delete nextRoot.hooks;
188
+ if (Object.keys(nextRoot).length > 0) {
189
+ return { nextContent: serializeCodexHooksConfig(nextRoot), removedCount };
190
+ }
191
+ return { nextContent: null, removedCount };
192
+ }
193
+ function findSectionBounds(lines, sectionHeader) {
194
+ const start = lines.findIndex((line) => line.trim() === sectionHeader);
195
+ if (start === -1) {
196
+ return null;
197
+ }
198
+ let end = lines.length;
199
+ for (let index = start + 1; index < lines.length; index += 1) {
200
+ if (/^\s*\[[^\]]+\]\s*$/.test(lines[index])) {
201
+ end = index;
202
+ break;
203
+ }
204
+ }
205
+ return { start, end };
206
+ }
207
+ export function enableCodexHooksFeature(existing) {
208
+ const trimmed = existing.trimEnd();
209
+ const lines = trimmed ? trimmed.split(/\r?\n/) : [];
210
+ const section = findSectionBounds(lines, "[features]");
211
+ if (!section) {
212
+ return trimmed
213
+ ? `${trimmed}\n\n[features]\ncodex_hooks = true\n`
214
+ : "[features]\ncodex_hooks = true\n";
215
+ }
216
+ const featureLines = lines.slice(section.start + 1, section.end);
217
+ const existingIndex = featureLines.findIndex((line) => /^\s*codex_hooks\s*=/.test(line));
218
+ if (existingIndex !== -1) {
219
+ featureLines[existingIndex] = "codex_hooks = true";
220
+ }
221
+ else {
222
+ featureLines.push("codex_hooks = true");
223
+ }
224
+ const rebuilt = [
225
+ ...lines.slice(0, section.start + 1),
226
+ ...featureLines,
227
+ ...lines.slice(section.end)
228
+ ].join("\n");
229
+ return `${rebuilt.trimEnd()}\n`;
230
+ }
231
+ export function codexHooksEnabled(config) {
232
+ const lines = config.split(/\r?\n/);
233
+ const section = findSectionBounds(lines, "[features]");
234
+ if (!section) {
235
+ return false;
236
+ }
237
+ return lines
238
+ .slice(section.start + 1, section.end)
239
+ .some((line) => /^\s*codex_hooks\s*=\s*true\s*$/.test(line));
240
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ export * from "./codex-hooks.js";
1
2
  export * from "./prompt-aliases.js";
2
3
  export * from "./personas.js";
3
4
  export * from "./persona-router.js";
4
5
  export * from "./panel.js";
5
6
  export * from "./project-session.js";
7
+ export * from "./question-obligations.js";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
+ export * from "./codex-hooks.js";
1
2
  export * from "./prompt-aliases.js";
2
3
  export * from "./personas.js";
3
4
  export * from "./persona-router.js";
4
5
  export * from "./panel.js";
5
6
  export * from "./project-session.js";
7
+ export * from "./question-obligations.js";
@@ -0,0 +1,4 @@
1
+ type CodexHookPayload = Record<string, unknown>;
2
+ export declare function dispatchCodexHook(payload: CodexHookPayload, cwdOverride?: string): Promise<Record<string, unknown> | null>;
3
+ export declare function isCodexNativeHookMainModule(moduleUrl: string, argv1: string | undefined): boolean;
4
+ export {};