@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 +195 -9
- package/dist/codex-hooks.d.ts +22 -0
- package/dist/codex-hooks.js +240 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/longtable-codex-native-hook.d.ts +4 -0
- package/dist/longtable-codex-native-hook.js +314 -0
- package/dist/project-session.d.ts +24 -1
- package/dist/project-session.js +180 -17
- package/dist/question-obligations.d.ts +22 -0
- package/dist/question-obligations.js +112 -0
- package/package.json +7 -7
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 {
|
|
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.
|
|
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
|
|
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
package/dist/index.js
CHANGED
|
@@ -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 {};
|