@longtable/cli 0.1.10 → 0.1.12
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 +329 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/project-session.d.ts +15 -1
- package/dist/project-session.js +131 -1
- package/package.json +7 -7
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
import { emitKeypressEvents } from "node:readline";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
@@ -14,7 +14,7 @@ import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodex
|
|
|
14
14
|
import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router.js";
|
|
15
15
|
import { PERSONA_DEFINITIONS, listRoleDefinitions } from "./personas.js";
|
|
16
16
|
import { buildPanelFallback, renderPanelSummary } from "./panel.js";
|
|
17
|
-
import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
|
|
17
|
+
import { appendInvocationRecordToWorkspace, assertWorkspaceNotBlocked, answerWorkspaceQuestion, createWorkspaceClarificationCard, createWorkspaceQuestion, createOrUpdateProjectWorkspace, inspectProjectWorkspace, loadProjectContextFromDirectory, renderProjectWorkspaceSummary, syncCurrentWorkspaceView } from "./project-session.js";
|
|
18
18
|
const VALID_MODES = new Set([
|
|
19
19
|
"explore",
|
|
20
20
|
"review",
|
|
@@ -39,6 +39,10 @@ const ANSI = {
|
|
|
39
39
|
cyan: "\u001B[36m",
|
|
40
40
|
green: "\u001B[32m"
|
|
41
41
|
};
|
|
42
|
+
const LONGTABLE_MCP_SERVER_NAME = "longtable-state";
|
|
43
|
+
const LONGTABLE_MCP_PACKAGE_VERSION = "0.1.11";
|
|
44
|
+
const LONGTABLE_MCP_MARKER_START = "# LongTable state MCP START";
|
|
45
|
+
const LONGTABLE_MCP_MARKER_END = "# LongTable state MCP END";
|
|
42
46
|
function style(text, prefix) {
|
|
43
47
|
return `${prefix}${text}${ANSI.reset}`;
|
|
44
48
|
}
|
|
@@ -80,7 +84,9 @@ function usage() {
|
|
|
80
84
|
" longtable roles [--json]",
|
|
81
85
|
" longtable show [--json] [--path <file>]",
|
|
82
86
|
" longtable install [--json] [--path <file>] [--runtime-path <file>]",
|
|
87
|
+
" longtable mcp install [--provider codex|claude|all] [--write] [--json] [--codex-config <path>] [--claude-settings <path>] [--package <spec>]",
|
|
83
88
|
" longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
|
|
89
|
+
" longtable clarify --prompt <task-context> [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json] [--force]",
|
|
84
90
|
" longtable question --prompt <decision-context> [--title <text>] [--text <question>] [--provider codex|claude] [--required|--advisory] [--print] [--cwd <path>] [--json]",
|
|
85
91
|
" 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>]",
|
|
86
92
|
" longtable decide [--question <id>] --answer <value-or-text> [--rationale <text>] [--provider codex|claude] [--cwd <path>] [--json]",
|
|
@@ -94,6 +100,7 @@ function usage() {
|
|
|
94
100
|
" longtable claude install-skills [--dir <path>]",
|
|
95
101
|
" longtable claude remove-skills [--dir <path>]",
|
|
96
102
|
" longtable claude status [--dir <path>] [--json]",
|
|
103
|
+
" longtable mcp install --provider all",
|
|
97
104
|
"",
|
|
98
105
|
"Examples:",
|
|
99
106
|
" longtable init --flow interview --provider codex --install-skills",
|
|
@@ -113,13 +120,13 @@ function parseArgs(argv) {
|
|
|
113
120
|
const values = {};
|
|
114
121
|
let subcommand = maybeSubcommand;
|
|
115
122
|
const modeCommand = command && VALID_MODES.has(command);
|
|
116
|
-
const directCommand = command && ["init", "start", "resume", "doctor", "status", "roles", "show", "install", "codex", "claude", "ask", "question", "panel", "decide"].includes(command);
|
|
123
|
+
const directCommand = command && ["init", "start", "resume", "doctor", "status", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "panel", "decide"].includes(command);
|
|
117
124
|
let startIndex = 1;
|
|
118
125
|
if (modeCommand) {
|
|
119
126
|
subcommand = undefined;
|
|
120
127
|
startIndex = 1;
|
|
121
128
|
}
|
|
122
|
-
else if (command === "codex" || command === "claude") {
|
|
129
|
+
else if (command === "codex" || command === "claude" || command === "mcp") {
|
|
123
130
|
startIndex = 2;
|
|
124
131
|
}
|
|
125
132
|
else if (directCommand) {
|
|
@@ -778,6 +785,147 @@ async function runInstall(args) {
|
|
|
778
785
|
}
|
|
779
786
|
console.log(renderInstallSummary(result));
|
|
780
787
|
}
|
|
788
|
+
function resolveMcpProviders(value) {
|
|
789
|
+
if (value === "codex" || value === "claude") {
|
|
790
|
+
return [value];
|
|
791
|
+
}
|
|
792
|
+
return ["codex", "claude"];
|
|
793
|
+
}
|
|
794
|
+
function resolveMcpPackageSpec(args) {
|
|
795
|
+
return typeof args.package === "string" && args.package.trim()
|
|
796
|
+
? args.package.trim()
|
|
797
|
+
: `@longtable/mcp@${LONGTABLE_MCP_PACKAGE_VERSION}`;
|
|
798
|
+
}
|
|
799
|
+
function resolveCodexMcpConfigPath(args) {
|
|
800
|
+
return resolve(normalizeUserPath(typeof args["codex-config"] === "string" && args["codex-config"].trim()
|
|
801
|
+
? args["codex-config"].trim()
|
|
802
|
+
: "~/.codex/config.toml"));
|
|
803
|
+
}
|
|
804
|
+
function resolveClaudeMcpSettingsPath(args) {
|
|
805
|
+
return resolve(normalizeUserPath(typeof args["claude-settings"] === "string" && args["claude-settings"].trim()
|
|
806
|
+
? args["claude-settings"].trim()
|
|
807
|
+
: "~/.claude/settings.json"));
|
|
808
|
+
}
|
|
809
|
+
function escapeTomlString(value) {
|
|
810
|
+
return JSON.stringify(value);
|
|
811
|
+
}
|
|
812
|
+
function renderCodexMcpBlock(serverName, command, mcpArgs) {
|
|
813
|
+
return [
|
|
814
|
+
LONGTABLE_MCP_MARKER_START,
|
|
815
|
+
`[mcp_servers.${serverName}]`,
|
|
816
|
+
`command = ${escapeTomlString(command)}`,
|
|
817
|
+
`args = [${mcpArgs.map((arg) => escapeTomlString(arg)).join(", ")}]`,
|
|
818
|
+
LONGTABLE_MCP_MARKER_END
|
|
819
|
+
].join("\n");
|
|
820
|
+
}
|
|
821
|
+
function replaceMarkedCodexMcpBlock(existing, block, serverName) {
|
|
822
|
+
const markerPattern = new RegExp(`${LONGTABLE_MCP_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${LONGTABLE_MCP_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "m");
|
|
823
|
+
const serverPattern = new RegExp(`\\n?\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[|$)`, "m");
|
|
824
|
+
const trimmed = existing.replace(markerPattern, "").replace(serverPattern, "").trimEnd();
|
|
825
|
+
return trimmed ? `${trimmed}\n\n${block}\n` : `${block}\n`;
|
|
826
|
+
}
|
|
827
|
+
async function writeCodexMcpConfig(path, block, serverName) {
|
|
828
|
+
const existing = existsSync(path) ? await readFile(path, "utf8") : "";
|
|
829
|
+
const updated = replaceMarkedCodexMcpBlock(existing, block, serverName);
|
|
830
|
+
await mkdir(dirname(path), { recursive: true });
|
|
831
|
+
await writeFile(path, updated, "utf8");
|
|
832
|
+
return updated;
|
|
833
|
+
}
|
|
834
|
+
function renderClaudeMcpJson(serverName, command, mcpArgs) {
|
|
835
|
+
return JSON.stringify({
|
|
836
|
+
mcpServers: {
|
|
837
|
+
[serverName]: {
|
|
838
|
+
command,
|
|
839
|
+
args: mcpArgs
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}, null, 2);
|
|
843
|
+
}
|
|
844
|
+
async function writeClaudeMcpSettings(path, serverName, command, mcpArgs) {
|
|
845
|
+
let settings = {};
|
|
846
|
+
if (existsSync(path)) {
|
|
847
|
+
const raw = await readFile(path, "utf8");
|
|
848
|
+
settings = raw.trim() ? JSON.parse(raw) : {};
|
|
849
|
+
}
|
|
850
|
+
const existingServers = typeof settings.mcpServers === "object" && settings.mcpServers !== null && !Array.isArray(settings.mcpServers)
|
|
851
|
+
? settings.mcpServers
|
|
852
|
+
: {};
|
|
853
|
+
settings.mcpServers = {
|
|
854
|
+
...existingServers,
|
|
855
|
+
[serverName]: {
|
|
856
|
+
command,
|
|
857
|
+
args: mcpArgs
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
const updated = JSON.stringify(settings, null, 2);
|
|
861
|
+
await mkdir(dirname(path), { recursive: true });
|
|
862
|
+
await writeFile(path, `${updated}\n`, "utf8");
|
|
863
|
+
return `${updated}\n`;
|
|
864
|
+
}
|
|
865
|
+
function renderMcpInstallSummary(result) {
|
|
866
|
+
const lines = [
|
|
867
|
+
"LongTable MCP transport",
|
|
868
|
+
`- server: ${result.serverName}`,
|
|
869
|
+
`- package: ${result.packageSpec}`,
|
|
870
|
+
`- command: ${result.command} ${result.args.join(" ")}`,
|
|
871
|
+
`- mode: ${result.write ? "wrote provider config" : "printed config only"}`,
|
|
872
|
+
""
|
|
873
|
+
];
|
|
874
|
+
for (const target of result.targets) {
|
|
875
|
+
lines.push(`${target.provider} (${target.path})`);
|
|
876
|
+
lines.push("```" + target.format);
|
|
877
|
+
lines.push(target.content.trimEnd());
|
|
878
|
+
lines.push("```");
|
|
879
|
+
lines.push("");
|
|
880
|
+
}
|
|
881
|
+
if (!result.write) {
|
|
882
|
+
lines.push("Run again with `--write` to update these provider config files.");
|
|
883
|
+
}
|
|
884
|
+
return lines.join("\n").trimEnd();
|
|
885
|
+
}
|
|
886
|
+
async function runMcpSubcommand(subcommand, args) {
|
|
887
|
+
if (!subcommand || subcommand === "install" || subcommand === "print-config") {
|
|
888
|
+
const serverName = typeof args.name === "string" && args.name.trim()
|
|
889
|
+
? args.name.trim()
|
|
890
|
+
: LONGTABLE_MCP_SERVER_NAME;
|
|
891
|
+
const packageSpec = resolveMcpPackageSpec(args);
|
|
892
|
+
const command = typeof args.command === "string" && args.command.trim() ? args.command.trim() : "npx";
|
|
893
|
+
const mcpArgs = command === "npx" ? ["-y", packageSpec] : [packageSpec];
|
|
894
|
+
const providers = resolveMcpProviders(args.provider);
|
|
895
|
+
const write = args.write === true;
|
|
896
|
+
const targets = [];
|
|
897
|
+
for (const provider of providers) {
|
|
898
|
+
if (provider === "codex") {
|
|
899
|
+
const path = resolveCodexMcpConfigPath(args);
|
|
900
|
+
const block = renderCodexMcpBlock(serverName, command, mcpArgs);
|
|
901
|
+
const content = write ? await writeCodexMcpConfig(path, block, serverName) : block;
|
|
902
|
+
targets.push({ provider, path, format: "toml", content });
|
|
903
|
+
}
|
|
904
|
+
if (provider === "claude") {
|
|
905
|
+
const path = resolveClaudeMcpSettingsPath(args);
|
|
906
|
+
const content = write
|
|
907
|
+
? await writeClaudeMcpSettings(path, serverName, command, mcpArgs)
|
|
908
|
+
: renderClaudeMcpJson(serverName, command, mcpArgs);
|
|
909
|
+
targets.push({ provider, path, format: "json", content });
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const result = {
|
|
913
|
+
serverName,
|
|
914
|
+
packageSpec,
|
|
915
|
+
command,
|
|
916
|
+
args: mcpArgs,
|
|
917
|
+
write,
|
|
918
|
+
targets
|
|
919
|
+
};
|
|
920
|
+
if (args.json === true) {
|
|
921
|
+
console.log(JSON.stringify(result, null, 2));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
console.log(renderMcpInstallSummary(result));
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
throw new Error("Unknown mcp subcommand.");
|
|
928
|
+
}
|
|
781
929
|
function commandOnPath(command) {
|
|
782
930
|
try {
|
|
783
931
|
execSync(`command -v ${command}`, { stdio: "ignore" });
|
|
@@ -1389,6 +1537,172 @@ async function runQuestion(args) {
|
|
|
1389
1537
|
console.log(`- answer: longtable decide --question ${result.question.id} --answer <value>`);
|
|
1390
1538
|
console.log(`- current: ${context.currentFilePath}`);
|
|
1391
1539
|
}
|
|
1540
|
+
function isInteractiveTerminal() {
|
|
1541
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
1542
|
+
}
|
|
1543
|
+
function questionRecordToChoices(record) {
|
|
1544
|
+
return [
|
|
1545
|
+
...record.prompt.options.map((option) => ({
|
|
1546
|
+
id: option.value,
|
|
1547
|
+
label: option.recommended ? `${option.label} (Recommended)` : option.label,
|
|
1548
|
+
description: option.description ?? "Select this option."
|
|
1549
|
+
})),
|
|
1550
|
+
...(record.prompt.allowOther
|
|
1551
|
+
? [{
|
|
1552
|
+
id: "other",
|
|
1553
|
+
label: record.prompt.otherLabel ?? "Other",
|
|
1554
|
+
description: "Type a custom answer.",
|
|
1555
|
+
fallbackToText: true
|
|
1556
|
+
}]
|
|
1557
|
+
: [])
|
|
1558
|
+
];
|
|
1559
|
+
}
|
|
1560
|
+
function renderClarificationCard(questions) {
|
|
1561
|
+
if (questions.length === 0) {
|
|
1562
|
+
return "No new clarification questions are pending for this prompt.";
|
|
1563
|
+
}
|
|
1564
|
+
const width = 44;
|
|
1565
|
+
const boxLine = (text = "") => `│ ${text.padEnd(width, " ")} │`;
|
|
1566
|
+
const wrap = (text) => {
|
|
1567
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
1568
|
+
const wrapped = [];
|
|
1569
|
+
let line = "";
|
|
1570
|
+
for (const word of words) {
|
|
1571
|
+
if (!line) {
|
|
1572
|
+
line = word;
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
if (`${line} ${word}`.length > width) {
|
|
1576
|
+
wrapped.push(line);
|
|
1577
|
+
line = word;
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
line = `${line} ${word}`;
|
|
1581
|
+
}
|
|
1582
|
+
if (line) {
|
|
1583
|
+
wrapped.push(line);
|
|
1584
|
+
}
|
|
1585
|
+
return wrapped.length > 0 ? wrapped : [""];
|
|
1586
|
+
};
|
|
1587
|
+
const lines = [
|
|
1588
|
+
"I want to make sure I handle this in the way you actually want, so here are the choices LongTable should not infer silently:",
|
|
1589
|
+
"",
|
|
1590
|
+
"┌──────────────────────────────────────────────┐"
|
|
1591
|
+
];
|
|
1592
|
+
for (const question of questions) {
|
|
1593
|
+
lines.push(boxLine(question.prompt.title));
|
|
1594
|
+
for (const line of wrap(question.prompt.question)) {
|
|
1595
|
+
lines.push(boxLine(line));
|
|
1596
|
+
}
|
|
1597
|
+
for (const option of question.prompt.options) {
|
|
1598
|
+
const suffix = option.recommended ? " (Recommended)" : "";
|
|
1599
|
+
for (const line of wrap(`- ${option.label}${suffix}`)) {
|
|
1600
|
+
lines.push(boxLine(line));
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (question.prompt.allowOther) {
|
|
1604
|
+
lines.push(boxLine(`- ${question.prompt.otherLabel ?? "Other"}`));
|
|
1605
|
+
}
|
|
1606
|
+
lines.push(boxLine());
|
|
1607
|
+
}
|
|
1608
|
+
lines.push("└──────────────────────────────────────────────┘");
|
|
1609
|
+
lines.push("");
|
|
1610
|
+
lines.push("Answer in a terminal with `longtable clarify --prompt ...`, or record choices with `longtable decide --question <id> --answer <value>`.");
|
|
1611
|
+
return lines.join("\n");
|
|
1612
|
+
}
|
|
1613
|
+
async function answerClarificationCardInTerminal(context, questions, provider) {
|
|
1614
|
+
if (questions.length === 0) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const rl = createInterface({ input, output });
|
|
1618
|
+
try {
|
|
1619
|
+
console.log(renderBrandBanner("LongTable", "Clarification Card"));
|
|
1620
|
+
console.log("");
|
|
1621
|
+
for (let index = 0; index < questions.length; index += 1) {
|
|
1622
|
+
const question = questions[index];
|
|
1623
|
+
const prompt = renderQuestionHeader(index + 1, questions.length, question.prompt.title, question.prompt.question);
|
|
1624
|
+
const answer = await promptChoice(rl, prompt, questionRecordToChoices(question));
|
|
1625
|
+
await answerWorkspaceQuestion({
|
|
1626
|
+
context,
|
|
1627
|
+
questionId: question.id,
|
|
1628
|
+
answer,
|
|
1629
|
+
provider,
|
|
1630
|
+
surface: "terminal_selector"
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
finally {
|
|
1635
|
+
rl.close();
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
async function runClarify(args) {
|
|
1639
|
+
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
1640
|
+
const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
|
|
1641
|
+
if (!prompt) {
|
|
1642
|
+
throw new Error("A task context is required. Pass --prompt <text>.");
|
|
1643
|
+
}
|
|
1644
|
+
const context = await loadProjectContextFromDirectory(workingDirectory);
|
|
1645
|
+
if (!context) {
|
|
1646
|
+
throw new Error("No LongTable project workspace was found here. Run this inside a project or pass --cwd.");
|
|
1647
|
+
}
|
|
1648
|
+
const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
|
|
1649
|
+
const required = args.required === true ? true : args.advisory === true ? false : undefined;
|
|
1650
|
+
const result = await createWorkspaceClarificationCard({
|
|
1651
|
+
context,
|
|
1652
|
+
prompt,
|
|
1653
|
+
provider,
|
|
1654
|
+
required,
|
|
1655
|
+
force: args.force === true
|
|
1656
|
+
});
|
|
1657
|
+
if (args.json === true) {
|
|
1658
|
+
console.log(JSON.stringify({
|
|
1659
|
+
questions: result.questions,
|
|
1660
|
+
created: result.created,
|
|
1661
|
+
alreadyAnswered: result.alreadyAnswered,
|
|
1662
|
+
files: {
|
|
1663
|
+
state: context.stateFilePath,
|
|
1664
|
+
current: context.currentFilePath
|
|
1665
|
+
}
|
|
1666
|
+
}, null, 2));
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
if (args.print === true || !isInteractiveTerminal()) {
|
|
1670
|
+
console.log(renderClarificationCard(result.questions));
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
await answerClarificationCardInTerminal(context, result.questions, provider);
|
|
1674
|
+
console.log("");
|
|
1675
|
+
console.log("LongTable clarification decisions recorded");
|
|
1676
|
+
console.log(`- answered: ${result.questions.length}`);
|
|
1677
|
+
console.log(`- state: ${context.stateFilePath}`);
|
|
1678
|
+
console.log(`- current: ${context.currentFilePath}`);
|
|
1679
|
+
}
|
|
1680
|
+
async function runAutomaticClarificationIfNeeded(prompt, args) {
|
|
1681
|
+
if (args["no-clarify"] === true || args.print === true || args.json === true) {
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1684
|
+
const workingDirectory = typeof args.cwd === "string" ? args.cwd : cwd();
|
|
1685
|
+
const context = await loadProjectContextFromDirectory(workingDirectory);
|
|
1686
|
+
if (!context) {
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
const provider = args.provider === "claude" ? "claude" : args.provider === "codex" ? "codex" : undefined;
|
|
1690
|
+
const result = await createWorkspaceClarificationCard({
|
|
1691
|
+
context,
|
|
1692
|
+
prompt,
|
|
1693
|
+
provider,
|
|
1694
|
+
required: true
|
|
1695
|
+
});
|
|
1696
|
+
if (result.questions.length === 0) {
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
if (!isInteractiveTerminal()) {
|
|
1700
|
+
console.log(renderClarificationCard(result.questions));
|
|
1701
|
+
return true;
|
|
1702
|
+
}
|
|
1703
|
+
await answerClarificationCardInTerminal(context, result.questions, provider);
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1392
1706
|
async function runAsk(args) {
|
|
1393
1707
|
const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
|
|
1394
1708
|
if (!prompt) {
|
|
@@ -1402,6 +1716,9 @@ async function runAsk(args) {
|
|
|
1402
1716
|
return;
|
|
1403
1717
|
}
|
|
1404
1718
|
const mode = inferred === "panel" ? "review" : inferred;
|
|
1719
|
+
if (await runAutomaticClarificationIfNeeded(effectivePrompt, args)) {
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1405
1722
|
const delegatedArgs = {
|
|
1406
1723
|
...args,
|
|
1407
1724
|
prompt: effectivePrompt
|
|
@@ -1728,10 +2045,18 @@ async function main() {
|
|
|
1728
2045
|
await runInstall(values);
|
|
1729
2046
|
return;
|
|
1730
2047
|
}
|
|
2048
|
+
if (command === "mcp") {
|
|
2049
|
+
await runMcpSubcommand(subcommand, values);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
1731
2052
|
if (command === "ask") {
|
|
1732
2053
|
await runAsk(values);
|
|
1733
2054
|
return;
|
|
1734
2055
|
}
|
|
2056
|
+
if (command === "clarify") {
|
|
2057
|
+
await runClarify(values);
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
1735
2060
|
if (command === "question") {
|
|
1736
2061
|
await runQuestion(values);
|
|
1737
2062
|
return;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DecisionRecord, InvocationRecord, ProviderKind, QuestionRecord, ResearchState } from "@longtable/core";
|
|
1
|
+
import type { DecisionRecord, InvocationRecord, ProviderKind, QuestionSurface, QuestionRecord, ResearchState } from "@longtable/core";
|
|
2
2
|
import type { SetupPersistedOutput } from "@longtable/setup";
|
|
3
3
|
export type ProjectDisagreementPreference = "synthesis_only" | "show_on_conflict" | "always_visible";
|
|
4
4
|
export interface LongTableProjectRecord {
|
|
@@ -99,10 +99,23 @@ export interface LongTableWorkspaceInspection {
|
|
|
99
99
|
timestamp: string;
|
|
100
100
|
}>;
|
|
101
101
|
}
|
|
102
|
+
export declare function loadWorkspaceState(context: LongTableProjectContext): Promise<ResearchState>;
|
|
102
103
|
export declare function syncCurrentWorkspaceView(context: LongTableProjectContext): Promise<string>;
|
|
103
104
|
export declare function appendInvocationRecordToWorkspace(context: LongTableProjectContext, invocation: InvocationRecord, questions?: QuestionRecord[]): Promise<ResearchState>;
|
|
104
105
|
export declare function listBlockingWorkspaceQuestions(context: LongTableProjectContext): Promise<QuestionRecord[]>;
|
|
105
106
|
export declare function assertWorkspaceNotBlocked(context: LongTableProjectContext): Promise<void>;
|
|
107
|
+
export declare function createWorkspaceClarificationCard(options: {
|
|
108
|
+
context: LongTableProjectContext;
|
|
109
|
+
prompt: string;
|
|
110
|
+
provider?: ProviderKind;
|
|
111
|
+
required?: boolean;
|
|
112
|
+
force?: boolean;
|
|
113
|
+
}): Promise<{
|
|
114
|
+
questions: QuestionRecord[];
|
|
115
|
+
state: ResearchState;
|
|
116
|
+
created: boolean;
|
|
117
|
+
alreadyAnswered: boolean;
|
|
118
|
+
}>;
|
|
106
119
|
export declare function createWorkspaceQuestion(options: {
|
|
107
120
|
context: LongTableProjectContext;
|
|
108
121
|
prompt: string;
|
|
@@ -120,6 +133,7 @@ export declare function answerWorkspaceQuestion(options: {
|
|
|
120
133
|
answer: string;
|
|
121
134
|
rationale?: string;
|
|
122
135
|
provider?: "codex" | "claude";
|
|
136
|
+
surface?: QuestionSurface;
|
|
123
137
|
}): Promise<{
|
|
124
138
|
question: QuestionRecord;
|
|
125
139
|
decision: DecisionRecord;
|
package/dist/project-session.js
CHANGED
|
@@ -205,6 +205,9 @@ async function loadResearchState(stateFilePath) {
|
|
|
205
205
|
...(parsed.studyContract ? { studyContract: parsed.studyContract } : {})
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
|
+
export async function loadWorkspaceState(context) {
|
|
209
|
+
return loadResearchState(context.stateFilePath);
|
|
210
|
+
}
|
|
208
211
|
function recentInvocationRecords(state, limit = 3) {
|
|
209
212
|
return (state.invocationLog ?? []).slice(-limit).reverse();
|
|
210
213
|
}
|
|
@@ -486,6 +489,133 @@ function optionsForCheckpointFamily(family) {
|
|
|
486
489
|
{ value: "defer", label: "Keep this open", description: "Do not commit yet; keep the issue visible as an open tension." }
|
|
487
490
|
];
|
|
488
491
|
}
|
|
492
|
+
function includesAny(prompt, patterns) {
|
|
493
|
+
return patterns.some((pattern) => pattern.test(prompt));
|
|
494
|
+
}
|
|
495
|
+
function clarificationOptions(first, second, third, fourth) {
|
|
496
|
+
return [first, second, third, ...(fourth ? [fourth] : [])];
|
|
497
|
+
}
|
|
498
|
+
function buildClarificationQuestionSpecs(prompt) {
|
|
499
|
+
const normalized = prompt.toLowerCase();
|
|
500
|
+
const specs = [];
|
|
501
|
+
function push(spec) {
|
|
502
|
+
if (!specs.some((candidate) => candidate.key === spec.key)) {
|
|
503
|
+
specs.push(spec);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (includesAny(normalized, [/\brubrics?\b/, /루브릭|채점기준/])) {
|
|
507
|
+
push({
|
|
508
|
+
key: "rubric_update_basis",
|
|
509
|
+
title: "Rubric update basis",
|
|
510
|
+
question: "How should LongTable use the available materials to update the rubric?",
|
|
511
|
+
whyNow: "Rubric updates can silently change grading criteria if LongTable guesses the calibration basis.",
|
|
512
|
+
options: clarificationOptions({ value: "calibrate_to_exemplars", label: "Calibrate criteria to exemplars", description: "Use strong submissions to refine what each criterion means.", recommended: true }, { value: "polish_existing", label: "Polish existing rubric only", description: "Keep criteria stable and improve wording or consistency." }, { value: "rewrite_structure", label: "Restructure the rubric", description: "Change categories or levels where the materials suggest a better structure." })
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (includesAny(normalized, [/\bexemplar\b/, /\bbest submission\b/, /\bselected submission\b/, /\bTA\b/i, /우수\s*답안|예시|선정|조교/])) {
|
|
516
|
+
push({
|
|
517
|
+
key: "exemplar_use",
|
|
518
|
+
title: "Exemplar use",
|
|
519
|
+
question: "How should LongTable use selected exemplars or TA guidance?",
|
|
520
|
+
whyNow: "Exemplars can either calibrate criteria privately or become visible evidence inside the output.",
|
|
521
|
+
options: clarificationOptions({ value: "calibrate_only", label: "Use as private calibration", description: "Adjust criteria using exemplars without quoting them.", recommended: true }, { value: "include_deidentified_excerpts", label: "Include de-identified excerpts", description: "Add short anonymized examples where they clarify quality." }, { value: "separate_notes", label: "Keep examples in separate notes", description: "Use exemplars outside the main artifact." })
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
if (includesAny(normalized, [/\binstruction/, /\bguidance\b/, /\bsource\b/, /\bfile\b/, /\bdocx?\b/, /지침|가이드|문서|파일|자료/])) {
|
|
525
|
+
push({
|
|
526
|
+
key: "source_authority",
|
|
527
|
+
title: "Source authority",
|
|
528
|
+
question: "If sources conflict or leave gaps, which source should LongTable privilege?",
|
|
529
|
+
whyNow: "Without an authority rule, LongTable may resolve conflicts by convenience rather than researcher intent.",
|
|
530
|
+
options: clarificationOptions({ value: "explicit_user_instruction", label: "Your explicit instruction", description: "Use the researcher's current instruction as the highest authority.", recommended: true }, { value: "project_files", label: "Project files", description: "Treat supplied files or existing artifacts as authoritative." }, { value: "external_guidance", label: "TA or external guidance", description: "Prioritize instructor, TA, venue, or policy guidance." })
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (includesAny(normalized, [/\bdeliver\b/, /\boutput\b/, /\btracked?[- ]?change/, /\bdocx?\b/, /\bmarkdown\b/, /\btable\b/, /전달|산출물|결과물|수정\s*표시|트랙|형식|포맷/])) {
|
|
534
|
+
push({
|
|
535
|
+
key: "delivery_format",
|
|
536
|
+
title: "Delivery format",
|
|
537
|
+
question: "How should LongTable deliver the clarified output?",
|
|
538
|
+
whyNow: "Format and change-tracking choices affect whether the result is usable for review or handoff.",
|
|
539
|
+
options: clarificationOptions({ value: "tracked_changes", label: "Tracked-change artifact", description: "Produce a reviewable changed version where possible.", recommended: true }, { value: "clean_final", label: "Clean final artifact", description: "Deliver the final version without change markup." }, { value: "summary_plus_artifact", label: "Summary plus artifact", description: "Include a concise change summary with the output." })
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
if (includesAny(normalized, [/\bupdate\b/, /\bchange\b/, /\bedit\b/, /\bfix\b/, /\bimplement\b/, /\bbuild\b/, /\bcreate\b/, /업데이트|수정|변경|구현|만들|고쳐/])) {
|
|
543
|
+
push({
|
|
544
|
+
key: "autonomy_boundary",
|
|
545
|
+
title: "Autonomy boundary",
|
|
546
|
+
question: "How much should LongTable do before checking back with you?",
|
|
547
|
+
whyNow: "Execution requests can move from advice to authorship or artifact ownership unless the boundary is explicit.",
|
|
548
|
+
options: clarificationOptions({ value: "ask_then_act", label: "Clarify first, then act", description: "Ask needed questions before changing the artifact.", recommended: true }, { value: "act_with_defaults", label: "Act with visible defaults", description: "Proceed using recommended defaults and record them." }, { value: "recommend_only", label: "Recommend only", description: "Describe changes but do not alter artifacts." })
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (includesAny(normalized, [/\bperformance\b/, /\btest\b/, /\bevaluate\b/, /\bcheck\b/, /\bbenchmark\b/, /성능|테스트|평가|체크|검증/])) {
|
|
552
|
+
push({
|
|
553
|
+
key: "evaluation_target",
|
|
554
|
+
title: "Evaluation target",
|
|
555
|
+
question: "What should LongTable treat as the main performance target?",
|
|
556
|
+
whyNow: "Performance checks can optimize for UX, correctness, trigger sensitivity, or delivery reliability.",
|
|
557
|
+
options: clarificationOptions({ value: "question_sensitivity", label: "Question sensitivity", description: "Check whether LongTable asks at the right knowledge-gap moments.", recommended: true }, { value: "renderer_convenience", label: "Renderer convenience", description: "Check whether the most convenient question UI is used." }, { value: "state_reliability", label: "State reliability", description: "Check whether questions and answers persist correctly." })
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
if (specs.length === 0) {
|
|
561
|
+
push({
|
|
562
|
+
key: "general_missing_context",
|
|
563
|
+
title: "Missing context",
|
|
564
|
+
question: "What should LongTable clarify before proceeding?",
|
|
565
|
+
whyNow: "The request can be answered in multiple ways, and choosing silently would hide a researcher judgment.",
|
|
566
|
+
options: clarificationOptions({ value: "scope", label: "Clarify scope first", description: "Ask what is included and excluded before acting.", recommended: true }, { value: "criteria", label: "Clarify success criteria", description: "Ask what would count as a good result." }, { value: "proceed", label: "Proceed with visible assumptions", description: "Continue, but make assumptions explicit." })
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
return specs;
|
|
570
|
+
}
|
|
571
|
+
const CLARIFICATION_PROMPT_PREFIX = "Clarification prompt:";
|
|
572
|
+
function hasClarificationPrompt(record, prompt) {
|
|
573
|
+
return record.prompt.rationale.includes(`${CLARIFICATION_PROMPT_PREFIX} ${prompt}`);
|
|
574
|
+
}
|
|
575
|
+
export async function createWorkspaceClarificationCard(options) {
|
|
576
|
+
const state = await loadResearchState(options.context.stateFilePath);
|
|
577
|
+
if (!options.force) {
|
|
578
|
+
const existing = (state.questionLog ?? []).filter((record) => hasClarificationPrompt(record, options.prompt));
|
|
579
|
+
const pending = existing.filter((record) => record.status === "pending");
|
|
580
|
+
if (pending.length > 0) {
|
|
581
|
+
return { questions: pending, state, created: false, alreadyAnswered: false };
|
|
582
|
+
}
|
|
583
|
+
if (existing.some((record) => record.status === "answered")) {
|
|
584
|
+
return { questions: [], state, created: false, alreadyAnswered: true };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const createdAt = nowIso();
|
|
588
|
+
const preferredSurfaces = options.provider === "claude"
|
|
589
|
+
? ["native_structured", "terminal_selector", "numbered"]
|
|
590
|
+
: ["terminal_selector", "numbered", "native_structured"];
|
|
591
|
+
const questions = buildClarificationQuestionSpecs(options.prompt).map((spec) => ({
|
|
592
|
+
id: createId("question_record"),
|
|
593
|
+
createdAt,
|
|
594
|
+
updatedAt: createdAt,
|
|
595
|
+
status: "pending",
|
|
596
|
+
prompt: {
|
|
597
|
+
id: createId("question_prompt"),
|
|
598
|
+
checkpointKey: `clarification_${spec.key}`,
|
|
599
|
+
title: spec.title,
|
|
600
|
+
question: spec.question,
|
|
601
|
+
type: "single_choice",
|
|
602
|
+
options: spec.options,
|
|
603
|
+
allowOther: true,
|
|
604
|
+
otherLabel: "Other",
|
|
605
|
+
required: options.required ?? true,
|
|
606
|
+
source: "runtime_guidance",
|
|
607
|
+
rationale: [
|
|
608
|
+
spec.whyNow,
|
|
609
|
+
`${CLARIFICATION_PROMPT_PREFIX} ${options.prompt}`
|
|
610
|
+
],
|
|
611
|
+
preferredSurfaces: preferredSurfaces
|
|
612
|
+
}
|
|
613
|
+
}));
|
|
614
|
+
const updated = appendQuestionRecords(state, questions);
|
|
615
|
+
await writeFile(options.context.stateFilePath, JSON.stringify(updated, null, 2), "utf8");
|
|
616
|
+
await syncCurrentWorkspaceView(options.context);
|
|
617
|
+
return { questions, state: updated, created: true, alreadyAnswered: false };
|
|
618
|
+
}
|
|
489
619
|
export async function createWorkspaceQuestion(options) {
|
|
490
620
|
const state = await loadResearchState(options.context.stateFilePath);
|
|
491
621
|
const trigger = classifyCheckpointTrigger(options.prompt, {
|
|
@@ -557,7 +687,7 @@ export async function answerWorkspaceQuestion(options) {
|
|
|
557
687
|
...(option || explicitOther ? {} : { otherText: options.answer }),
|
|
558
688
|
...(options.rationale ? { rationale: options.rationale } : {}),
|
|
559
689
|
...(options.provider ? { provider: options.provider } : {}),
|
|
560
|
-
surface: options.provider === "claude" ? "native_structured" : "numbered"
|
|
690
|
+
surface: options.surface ?? (options.provider === "claude" ? "native_structured" : "numbered")
|
|
561
691
|
};
|
|
562
692
|
const timestamp = nowIso();
|
|
563
693
|
const decision = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Researcher-facing LongTable CLI",
|
|
6
6
|
"type": "module",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@longtable/checkpoints": "0.1.
|
|
32
|
-
"@longtable/core": "0.1.
|
|
33
|
-
"@longtable/memory": "0.1.
|
|
34
|
-
"@longtable/provider-claude": "0.1.
|
|
35
|
-
"@longtable/provider-codex": "0.1.
|
|
36
|
-
"@longtable/setup": "0.1.
|
|
31
|
+
"@longtable/checkpoints": "0.1.12",
|
|
32
|
+
"@longtable/core": "0.1.12",
|
|
33
|
+
"@longtable/memory": "0.1.12",
|
|
34
|
+
"@longtable/provider-claude": "0.1.12",
|
|
35
|
+
"@longtable/provider-codex": "0.1.12",
|
|
36
|
+
"@longtable/setup": "0.1.12"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^22.10.1",
|