@longtable/cli 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -2,26 +2,22 @@
2
2
 
3
3
  Researcher-facing Long Table CLI built on top of the legacy `@diverga/*` package surface.
4
4
 
5
- 중요한 현재 상태:
6
-
7
- - `@longtable/cli`는 repo 안에서는 구현되어 있음
8
- - npm의 `@longtable` scope가 아직 준비되지 않아 실제 publish는 아직 안 됨
9
- - 지금 공개 설치 경로는 여전히 `@diverga/setup`, `@diverga/provider-codex`
5
+ 현재 공개 설치 경로는 `@longtable/cli`입니다.
10
6
 
11
7
  ## Install
12
8
 
13
- 로컬 preview:
9
+ 공개 설치:
14
10
 
15
11
  ```bash
16
- npm install
17
- npm run build
18
- node packages/longtable/dist/cli.js --help
12
+ npm install -g @longtable/cli
19
13
  ```
20
14
 
21
- 예상 공개 설치 경로:
15
+ 로컬 preview:
22
16
 
23
17
  ```bash
24
- npm install -g @longtable/cli
18
+ npm install
19
+ npm run build
20
+ node packages/longtable/dist/cli.js --help
25
21
  ```
26
22
 
27
23
  ## Recommended flow
@@ -29,14 +25,20 @@ npm install -g @longtable/cli
29
25
  Run setup once and install Codex prompt aliases:
30
26
 
31
27
  ```bash
32
- longtable init --install-prompts
28
+ longtable init --flow interview --install-prompts
33
29
  ```
34
30
 
31
+ `longtable init` now uses an arrow-key terminal menu instead of plain number-entry prompts and supports:
32
+
33
+ - `--flow quickstart`
34
+ - `--flow interview`
35
+
35
36
  Then you can work in two ways.
36
37
 
37
38
  From the terminal:
38
39
 
39
40
  ```bash
41
+ longtable ask --prompt "연구를 시작하고 싶어. 지금 어디서부터 좁혀야 할지 모르겠어."
40
42
  longtable review --prompt "Review this claim critically."
41
43
  longtable review --prompt "BJET 편집자 관점에서 봐줘." --role editor
42
44
  longtable review --prompt "방법론적으로 어디가 취약한지 말해줘." --panel --show-conflicts
@@ -44,6 +46,7 @@ longtable review --prompt "방법론적으로 어디가 취약한지 말해줘."
44
46
 
45
47
  Inside Codex after prompt aliases are installed:
46
48
 
49
+ - `/prompts:longtable`
47
50
  - `/prompts:longtable-init`
48
51
  - `/prompts:longtable-explore`
49
52
  - `/prompts:longtable-review`
@@ -55,6 +58,7 @@ Inside Codex after prompt aliases are installed:
55
58
  ## Core commands
56
59
 
57
60
  ```bash
61
+ longtable ask --prompt "Help me open this research problem carefully."
58
62
  longtable init
59
63
  longtable show --json
60
64
  longtable install --json
@@ -71,6 +75,18 @@ Install Codex prompt aliases:
71
75
  longtable codex install-prompts
72
76
  ```
73
77
 
78
+ If you finish onboarding inside Codex with `/prompts:longtable-init`, you can persist the collected answers with:
79
+
80
+ ```bash
81
+ longtable codex persist-init --flow interview --provider codex --field education --career-stage doctoral --experience intermediate --project-type "journal article" --checkpoint balanced --topic "AI adoption in workplace settings" --blocker "기업 맥락으로 범위를 다시 잡아야 하는데 어디서부터 포함/제외 기준을 세울지 모르겠어." --entry-mode explore --weakest-domain methodology --panel-preference show_on_conflict --install-prompts
82
+ ```
83
+
84
+ Or from a JSON block:
85
+
86
+ ```bash
87
+ pbpaste | longtable codex persist-init --stdin --install-prompts
88
+ ```
89
+
74
90
  Check whether setup and aliases are present:
75
91
 
76
92
  ```bash
@@ -86,6 +102,6 @@ longtable codex remove-prompts
86
102
  ## Why this package exists
87
103
 
88
104
  Long Table is the product name.
89
- `Diverga` is still the legacy technical identifier for package names, CLI binaries, and runtime paths.
105
+ `Diverga` remains an internal compatibility layer for some lower-level packages and runtime paths.
90
106
 
91
- This package creates a researcher-facing command surface now, without forcing a breaking rename of the published package chain yet.
107
+ This package exists so researchers can install and run `longtable` directly while that compatibility layer remains underneath.
package/bin/longtable CHANGED
File without changes
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync } from "node:fs";
3
+ import { emitKeypressEvents } from "node:readline";
3
4
  import { createInterface } from "node:readline/promises";
4
5
  import { stdin as input, stdout as output, cwd, exit } from "node:process";
5
6
  import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupAndRuntimeConfig, serializeSetupOutput } from "@diverga/setup";
@@ -26,18 +27,22 @@ const VALID_STAGES = new Set([
26
27
  function usage() {
27
28
  return [
28
29
  "Usage:",
29
- " longtable init [--provider codex|claude] [--field <field>] [--career-stage <stage>] [--experience novice|intermediate|advanced] [--project-type <type>] [--checkpoint low|balanced|high] [--authorship-signal <text>] [--json] [--no-install] [--install-prompts]",
30
+ " longtable init [--flow quickstart|interview] [--provider codex|claude] [--field <field>] [--career-stage <stage>] [--experience novice|intermediate|advanced] [--project-type <type>] [--checkpoint low|balanced|high] [--authorship-signal <text>] [--topic <text>] [--blocker <text>] [--entry-mode explore|review|critique|draft|commit] [--weakest-domain theory|methodology|measurement|analysis|writing] [--panel-preference synthesis_only|show_on_conflict|always_visible] [--json] [--no-install] [--install-prompts]",
30
31
  " longtable show [--json] [--path <file>]",
31
32
  " longtable install [--json] [--path <file>] [--runtime-path <file>]",
33
+ " longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
32
34
  " 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>]",
35
+ " longtable codex persist-init [--answers-json <json> | --stdin | full setup flags] [--install-prompts] [--json]",
33
36
  " longtable codex install-prompts [--dir <path>]",
34
37
  " longtable codex remove-prompts [--dir <path>]",
35
38
  " longtable codex status [--dir <path>] [--json]",
36
39
  "",
37
40
  "Examples:",
38
- " longtable init --install-prompts",
41
+ " longtable init --flow interview --install-prompts",
42
+ " longtable ask --prompt \"연구를 시작하고 싶어. 지금 어디서부터 좁혀야 할지 모르겠어.\"",
39
43
  " longtable review --prompt \"Review this claim critically.\" --panel --show-conflicts",
40
44
  " longtable review --role editor --prompt \"BJET 편집자 관점에서 봐줘.\"",
45
+ " printf '{\"provider\":\"codex\",...}' | longtable codex persist-init --stdin --install-prompts",
41
46
  " longtable codex install-prompts"
42
47
  ].join("\n");
43
48
  }
@@ -46,7 +51,7 @@ function parseArgs(argv) {
46
51
  const values = {};
47
52
  let subcommand = maybeSubcommand;
48
53
  const modeCommand = command && VALID_MODES.has(command);
49
- const directCommand = command && ["init", "show", "install", "codex"].includes(command);
54
+ const directCommand = command && ["init", "show", "install", "codex", "ask"].includes(command);
50
55
  let startIndex = 1;
51
56
  if (modeCommand) {
52
57
  subcommand = undefined;
@@ -88,7 +93,65 @@ function renderChoices(choices) {
88
93
  .map((choice, index) => `${index + 1}. ${choice.label} — ${choice.description}`)
89
94
  .join("\n");
90
95
  }
91
- async function promptChoice(rl, prompt, choices) {
96
+ function buildSetupFlowChoices() {
97
+ return [
98
+ {
99
+ id: "quickstart",
100
+ label: "Quickstart",
101
+ description: "Minimal setup for the fastest first win."
102
+ },
103
+ {
104
+ id: "interview",
105
+ label: "Interview",
106
+ description: "A more detailed researcher profile interview for better first guidance."
107
+ }
108
+ ];
109
+ }
110
+ function renderSetupHeader(flow) {
111
+ const title = flow === "interview" ? "Long Table Setup Interview" : "Long Table Quickstart";
112
+ const subtitle = flow === "interview"
113
+ ? "We will ask about your research persona, current topic, blocker, and preferred kind of challenge."
114
+ : "We will capture the minimum profile needed to start using Long Table.";
115
+ return [
116
+ "┌──────────────────────────────────────────────┐",
117
+ `│ ${title.padEnd(44, " ")}│`,
118
+ "└──────────────────────────────────────────────┘",
119
+ subtitle
120
+ ].join("\n");
121
+ }
122
+ function renderQuestionHeader(index, total, section, prompt) {
123
+ return [``, `[${index}/${total}] ${section}`, prompt].join("\n");
124
+ }
125
+ function questionSection(questionId) {
126
+ if (questionId === "field" || questionId === "careerStage" || questionId === "experienceLevel") {
127
+ return "Researcher profile";
128
+ }
129
+ if (questionId === "currentProjectType" || questionId === "currentResearchTopic" || questionId === "currentBlocker") {
130
+ return "Current work";
131
+ }
132
+ if (questionId === "preferredCheckpointIntensity" || questionId === "preferredEntryMode") {
133
+ return "Interaction style";
134
+ }
135
+ if (questionId === "weakestDomain" || questionId === "panelPreference") {
136
+ return "How Long Table should challenge you";
137
+ }
138
+ return "Authorship and voice";
139
+ }
140
+ function moveCursorUp(lines) {
141
+ return lines > 0 ? `\u001B[${lines}A` : "";
142
+ }
143
+ function clearLine() {
144
+ return "\u001B[2K\r";
145
+ }
146
+ function renderArrowMenu(prompt, choices, selectedIndex) {
147
+ const lines = [prompt, "Use ↑/↓ and Enter."];
148
+ for (let index = 0; index < choices.length; index += 1) {
149
+ const prefix = index === selectedIndex ? ">" : " ";
150
+ lines.push(`${prefix} ${choices[index].label} - ${choices[index].description}`);
151
+ }
152
+ return lines.join("\n");
153
+ }
154
+ async function promptChoiceByNumber(rl, prompt, choices) {
92
155
  while (true) {
93
156
  const answer = await rl.question(`${prompt}\n${renderChoices(choices)}\nSelect one number: `);
94
157
  const numeric = Number(answer.trim());
@@ -108,10 +171,94 @@ async function promptChoice(rl, prompt, choices) {
108
171
  return choice.id;
109
172
  }
110
173
  }
174
+ async function promptText(rl, prompt, required) {
175
+ while (true) {
176
+ const answer = (await rl.question(`${prompt}\n> `)).trim();
177
+ if (!required) {
178
+ return answer || undefined;
179
+ }
180
+ if (answer) {
181
+ return answer;
182
+ }
183
+ console.log("This answer cannot be empty.");
184
+ }
185
+ }
186
+ async function promptChoiceWithArrows(rl, prompt, choices) {
187
+ const stream = input;
188
+ if (!stream.isTTY || !output.isTTY) {
189
+ return promptChoiceByNumber(rl, prompt, choices);
190
+ }
191
+ const previousRawMode = stream.isRaw;
192
+ let selectedIndex = 0;
193
+ const renderedLineCount = choices.length + 2;
194
+ return await new Promise((resolve, reject) => {
195
+ function draw(first = false) {
196
+ if (!first) {
197
+ output.write(moveCursorUp(renderedLineCount));
198
+ }
199
+ const rendered = renderArrowMenu(prompt, choices, selectedIndex).split("\n");
200
+ for (const line of rendered) {
201
+ output.write(clearLine());
202
+ output.write(`${line}\n`);
203
+ }
204
+ }
205
+ function cleanup() {
206
+ stream.off("keypress", onKeypress);
207
+ if (stream.isTTY) {
208
+ stream.setRawMode(previousRawMode ?? false);
209
+ }
210
+ output.write("\u001B[?25h");
211
+ }
212
+ async function handleChoice(choice) {
213
+ cleanup();
214
+ if (choice.fallbackToText) {
215
+ const freeText = await rl.question("Type your custom value: ");
216
+ if (!freeText.trim()) {
217
+ resolve(await promptChoiceWithArrows(rl, prompt, choices));
218
+ return;
219
+ }
220
+ resolve(freeText.trim());
221
+ return;
222
+ }
223
+ resolve(choice.id);
224
+ }
225
+ function onKeypress(_, key) {
226
+ if (key.ctrl && key.name === "c") {
227
+ cleanup();
228
+ reject(new Error("Setup cancelled."));
229
+ return;
230
+ }
231
+ if (key.name === "up") {
232
+ selectedIndex = selectedIndex === 0 ? choices.length - 1 : selectedIndex - 1;
233
+ draw();
234
+ return;
235
+ }
236
+ if (key.name === "down") {
237
+ selectedIndex = selectedIndex === choices.length - 1 ? 0 : selectedIndex + 1;
238
+ draw();
239
+ return;
240
+ }
241
+ if (key.name === "return") {
242
+ void handleChoice(choices[selectedIndex]);
243
+ }
244
+ }
245
+ emitKeypressEvents(stream);
246
+ stream.setRawMode(true);
247
+ output.write("\u001B[?25l");
248
+ draw(true);
249
+ stream.on("keypress", onKeypress);
250
+ });
251
+ }
252
+ async function promptChoice(rl, prompt, choices) {
253
+ return promptChoiceWithArrows(rl, prompt, choices);
254
+ }
111
255
  function hasCompleteFlagInput(args) {
112
256
  const required = ["provider", "field", "career-stage", "experience", "project-type", "checkpoint"];
113
257
  return required.every((key) => typeof args[key] === "string" && String(args[key]).trim().length > 0);
114
258
  }
259
+ function resolveSetupFlow(args) {
260
+ return String(args.flow) === "interview" ? "interview" : "quickstart";
261
+ }
115
262
  function toSetupAnswers(args) {
116
263
  return {
117
264
  field: String(args.field),
@@ -121,19 +268,43 @@ function toSetupAnswers(args) {
121
268
  preferredCheckpointIntensity: String(args.checkpoint),
122
269
  humanAuthorshipSignal: typeof args["authorship-signal"] === "string" && args["authorship-signal"].trim().length > 0
123
270
  ? args["authorship-signal"].trim()
271
+ : undefined,
272
+ currentResearchTopic: typeof args.topic === "string" && args.topic.trim().length > 0 ? args.topic.trim() : undefined,
273
+ currentBlocker: typeof args.blocker === "string" && args.blocker.trim().length > 0 ? args.blocker.trim() : undefined,
274
+ preferredEntryMode: typeof args["entry-mode"] === "string" && VALID_MODES.has(String(args["entry-mode"]))
275
+ ? String(args["entry-mode"])
276
+ : undefined,
277
+ weakestDomain: typeof args["weakest-domain"] === "string"
278
+ ? String(args["weakest-domain"])
279
+ : undefined,
280
+ panelPreference: typeof args["panel-preference"] === "string"
281
+ ? String(args["panel-preference"])
124
282
  : undefined
125
283
  };
126
284
  }
127
285
  async function collectInteractiveAnswers() {
128
286
  const rl = createInterface({ input, output });
129
287
  try {
288
+ const flow = (await promptChoice(rl, "How would you like to set up Long Table?", buildSetupFlowChoices()));
289
+ console.log("");
290
+ console.log(renderSetupHeader(flow));
291
+ console.log("");
130
292
  const provider = await promptChoice(rl, "Which provider do you want to configure?", buildProviderChoices());
131
293
  const answers = {};
132
- for (const question of buildQuickSetupFlow()) {
133
- if (!question.choices) {
294
+ const questions = buildQuickSetupFlow(flow);
295
+ for (let index = 0; index < questions.length; index += 1) {
296
+ const question = questions[index];
297
+ const prompt = renderQuestionHeader(index + 1, questions.length, questionSection(question.id), question.prompt);
298
+ let value;
299
+ if (question.kind === "text") {
300
+ value = await promptText(rl, prompt, question.required);
301
+ }
302
+ else if (question.choices) {
303
+ value = await promptChoice(rl, prompt, question.choices);
304
+ }
305
+ if (!value) {
134
306
  continue;
135
307
  }
136
- const value = await promptChoice(rl, question.prompt, question.choices);
137
308
  if (question.id === "field")
138
309
  answers.field = value;
139
310
  if (question.id === "careerStage")
@@ -148,8 +319,19 @@ async function collectInteractiveAnswers() {
148
319
  if (question.id === "humanAuthorshipSignal" && value !== "other") {
149
320
  answers.humanAuthorshipSignal = value;
150
321
  }
322
+ if (question.id === "currentResearchTopic")
323
+ answers.currentResearchTopic = value;
324
+ if (question.id === "currentBlocker")
325
+ answers.currentBlocker = value;
326
+ if (question.id === "preferredEntryMode")
327
+ answers.preferredEntryMode = value;
328
+ if (question.id === "weakestDomain")
329
+ answers.weakestDomain = value;
330
+ if (question.id === "panelPreference")
331
+ answers.panelPreference = value;
151
332
  }
152
333
  return {
334
+ flow,
153
335
  provider,
154
336
  answers: answers
155
337
  };
@@ -158,6 +340,61 @@ async function collectInteractiveAnswers() {
158
340
  rl.close();
159
341
  }
160
342
  }
343
+ function normalizePersistAnswers(raw) {
344
+ return {
345
+ flow: raw.flow === "interview" ? "interview" : "quickstart",
346
+ provider: raw.provider === "claude" ? "claude" : "codex",
347
+ answers: {
348
+ field: raw.field,
349
+ careerStage: raw.careerStage,
350
+ experienceLevel: raw.experienceLevel,
351
+ currentProjectType: raw.currentProjectType,
352
+ preferredCheckpointIntensity: raw.preferredCheckpointIntensity,
353
+ ...(raw.humanAuthorshipSignal?.trim()
354
+ ? { humanAuthorshipSignal: raw.humanAuthorshipSignal.trim() }
355
+ : {}),
356
+ ...(raw.currentResearchTopic?.trim()
357
+ ? { currentResearchTopic: raw.currentResearchTopic.trim() }
358
+ : {}),
359
+ ...(raw.currentBlocker?.trim()
360
+ ? { currentBlocker: raw.currentBlocker.trim() }
361
+ : {}),
362
+ ...(raw.preferredEntryMode
363
+ ? { preferredEntryMode: raw.preferredEntryMode }
364
+ : {}),
365
+ ...(raw.weakestDomain
366
+ ? { weakestDomain: raw.weakestDomain }
367
+ : {}),
368
+ ...(raw.panelPreference
369
+ ? { panelPreference: raw.panelPreference }
370
+ : {})
371
+ }
372
+ };
373
+ }
374
+ async function readPersistAnswers(args) {
375
+ if (typeof args["answers-json"] === "string") {
376
+ const normalized = normalizePersistAnswers(JSON.parse(args["answers-json"]));
377
+ return {
378
+ ...normalized,
379
+ flow: typeof args.flow === "string" ? resolveSetupFlow(args) : normalized.flow
380
+ };
381
+ }
382
+ if (args.stdin === true) {
383
+ const raw = readFileSync(0, "utf8").trim();
384
+ if (!raw) {
385
+ throw new Error("No JSON was provided on stdin.");
386
+ }
387
+ return normalizePersistAnswers(JSON.parse(raw));
388
+ }
389
+ if (hasCompleteFlagInput(args)) {
390
+ return {
391
+ flow: resolveSetupFlow(args),
392
+ provider: String(args.provider) === "claude" ? "claude" : "codex",
393
+ answers: toSetupAnswers(args)
394
+ };
395
+ }
396
+ throw new Error("persist-init requires either --answers-json, --stdin, or the full set of setup flags.");
397
+ }
161
398
  async function runInit(args) {
162
399
  const json = args.json === true;
163
400
  const installRuntime = args["no-install"] !== true;
@@ -165,13 +402,14 @@ async function runInit(args) {
165
402
  const customPath = typeof args.path === "string" ? args.path : undefined;
166
403
  const runtimePath = typeof args["runtime-path"] === "string" ? args["runtime-path"] : undefined;
167
404
  const promptsDir = typeof args.dir === "string" ? args.dir : undefined;
168
- const { provider, answers } = hasCompleteFlagInput(args)
405
+ const { flow, provider, answers } = hasCompleteFlagInput(args)
169
406
  ? {
407
+ flow: resolveSetupFlow(args),
170
408
  provider: String(args.provider) === "claude" ? "claude" : "codex",
171
409
  answers: toSetupAnswers(args)
172
410
  }
173
411
  : await collectInteractiveAnswers();
174
- const outputValue = createPersistedSetupOutput(answers, provider);
412
+ const outputValue = createPersistedSetupOutput(answers, provider, flow);
175
413
  const result = await saveSetupAndRuntimeConfig(outputValue, {
176
414
  setupPath: customPath,
177
415
  runtimePath
@@ -196,6 +434,20 @@ async function runInit(args) {
196
434
  console.log(`- /prompts:${prompt.name}`);
197
435
  }
198
436
  }
437
+ if (provider === "codex") {
438
+ console.log("");
439
+ console.log("Next step:");
440
+ console.log("- Open Codex and start with `/prompts:longtable`.");
441
+ if (answers.currentBlocker) {
442
+ console.log(`- Suggested first message: ${answers.currentBlocker}`);
443
+ }
444
+ else if (answers.currentResearchTopic) {
445
+ console.log(`- Suggested first message: Help me open the problem around ${answers.currentResearchTopic}.`);
446
+ }
447
+ else {
448
+ console.log("- Suggested first message: I want to start my research and need help opening the problem.");
449
+ }
450
+ }
199
451
  }
200
452
  async function runShow(args) {
201
453
  const outputValue = await loadSetupOutput(typeof args.path === "string" ? args.path : undefined);
@@ -216,6 +468,50 @@ async function runInstall(args) {
216
468
  }
217
469
  console.log(renderInstallSummary(result));
218
470
  }
471
+ async function runCodexPersistInit(args) {
472
+ const { flow, provider, answers } = await readPersistAnswers(args);
473
+ const outputValue = createPersistedSetupOutput(answers, provider, flow);
474
+ const result = await saveSetupAndRuntimeConfig(outputValue, {
475
+ setupPath: typeof args.path === "string" ? args.path : undefined,
476
+ runtimePath: typeof args["runtime-path"] === "string" ? args["runtime-path"] : undefined
477
+ });
478
+ let installedPrompts = [];
479
+ if (provider === "codex" && args["install-prompts"] === true) {
480
+ installedPrompts = await installCodexPromptAliases(typeof args.dir === "string" ? args.dir : undefined);
481
+ }
482
+ if (args.json === true) {
483
+ console.log(JSON.stringify({
484
+ setup: outputValue,
485
+ install: result,
486
+ installedPrompts: installedPrompts.map((prompt) => prompt.name)
487
+ }, null, 2));
488
+ return;
489
+ }
490
+ console.log(renderSetupSummary(outputValue));
491
+ console.log("");
492
+ console.log(renderInstallSummary(result));
493
+ if (installedPrompts.length > 0) {
494
+ console.log("");
495
+ console.log("Installed Codex prompt aliases:");
496
+ for (const prompt of installedPrompts) {
497
+ console.log(`- /prompts:${prompt.name}`);
498
+ }
499
+ }
500
+ if (provider === "codex") {
501
+ console.log("");
502
+ console.log("Next step:");
503
+ console.log("- Start Codex and use `/prompts:longtable` for the most natural entry.");
504
+ if (answers.currentBlocker) {
505
+ console.log(`- Suggested first message: ${answers.currentBlocker}`);
506
+ }
507
+ else if (answers.currentResearchTopic) {
508
+ console.log(`- Suggested first message: Help me think through ${answers.currentResearchTopic}.`);
509
+ }
510
+ else {
511
+ console.log("- Suggested first message: I want to start my research and I am not sure where to open the problem.");
512
+ }
513
+ }
514
+ }
219
515
  async function resolvePrompt(prompt) {
220
516
  if (prompt?.trim()) {
221
517
  return prompt.trim();
@@ -231,6 +527,57 @@ async function resolvePrompt(prompt) {
231
527
  rl.close();
232
528
  }
233
529
  }
530
+ function inferModeFromPrompt(prompt) {
531
+ const normalized = prompt.toLowerCase();
532
+ if (normalized.includes("status") ||
533
+ normalized.includes("설정") ||
534
+ normalized.includes("상태") ||
535
+ normalized.includes("롱테이블 상태")) {
536
+ return "status";
537
+ }
538
+ if (normalized.includes("패널") ||
539
+ normalized.includes("의견 충돌") ||
540
+ normalized.includes("conflict") ||
541
+ normalized.includes("disagree") ||
542
+ normalized.includes("panel")) {
543
+ return "panel";
544
+ }
545
+ if (normalized.includes("결정") ||
546
+ normalized.includes("고를") ||
547
+ normalized.includes("선택") ||
548
+ normalized.includes("commit")) {
549
+ return "commit";
550
+ }
551
+ if (normalized.includes("초안") ||
552
+ normalized.includes("써줘") ||
553
+ normalized.includes("문단") ||
554
+ normalized.includes("draft") ||
555
+ normalized.includes("write")) {
556
+ return "draft";
557
+ }
558
+ if (normalized.includes("비판") ||
559
+ normalized.includes("반론") ||
560
+ normalized.includes("critique") ||
561
+ normalized.includes("challenge")) {
562
+ return "critique";
563
+ }
564
+ if (normalized.includes("검토") ||
565
+ normalized.includes("review") ||
566
+ normalized.includes("편집자") ||
567
+ normalized.includes("리뷰어") ||
568
+ normalized.includes("judge")) {
569
+ return "review";
570
+ }
571
+ return "explore";
572
+ }
573
+ async function loadOptionalSetup(path) {
574
+ try {
575
+ return await loadSetupOutput(path);
576
+ }
577
+ catch {
578
+ return null;
579
+ }
580
+ }
234
581
  async function runModeCommand(mode, args) {
235
582
  const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
236
583
  if (!prompt) {
@@ -240,10 +587,16 @@ async function runModeCommand(mode, args) {
240
587
  if (stage && !VALID_STAGES.has(stage)) {
241
588
  throw new Error(`Invalid stage: ${stage}`);
242
589
  }
590
+ const setup = await loadOptionalSetup(typeof args.setup === "string" ? args.setup : undefined);
591
+ const panelPreference = setup?.profileSeed.panelPreference;
592
+ const panelRequested = args.panel === true ||
593
+ panelPreference === "always_visible" ||
594
+ (panelPreference === "show_on_conflict" && args["show-conflicts"] === true);
243
595
  const { guidedPrompt } = buildPersonaGuidance({
596
+ mode,
244
597
  prompt,
245
598
  roleFlag: typeof args.role === "string" ? args.role : undefined,
246
- panel: args.panel === true,
599
+ panel: panelRequested,
247
600
  showConflicts: args["show-conflicts"] === true,
248
601
  showDeliberation: args["show-deliberation"] === true
249
602
  });
@@ -268,6 +621,27 @@ async function runModeCommand(mode, args) {
268
621
  });
269
622
  exit(exitCode);
270
623
  }
624
+ async function runAsk(args) {
625
+ const prompt = await resolvePrompt(typeof args.prompt === "string" ? args.prompt : undefined);
626
+ if (!prompt) {
627
+ throw new Error("A prompt is required.");
628
+ }
629
+ const inferred = inferModeFromPrompt(prompt);
630
+ if (inferred === "status") {
631
+ await runCodexSubcommand("status", args);
632
+ return;
633
+ }
634
+ const mode = inferred === "panel" ? "review" : inferred;
635
+ const delegatedArgs = {
636
+ ...args,
637
+ prompt
638
+ };
639
+ if (inferred === "panel" && delegatedArgs.panel !== true) {
640
+ delegatedArgs.panel = true;
641
+ delegatedArgs["show-conflicts"] = true;
642
+ }
643
+ await runModeCommand(mode, delegatedArgs);
644
+ }
271
645
  async function runCodexSubcommand(subcommand, args) {
272
646
  const customDir = typeof args.dir === "string" ? args.dir : undefined;
273
647
  if (subcommand === "install-prompts") {
@@ -278,6 +652,10 @@ async function runCodexSubcommand(subcommand, args) {
278
652
  }
279
653
  return;
280
654
  }
655
+ if (subcommand === "persist-init") {
656
+ await runCodexPersistInit(args);
657
+ return;
658
+ }
281
659
  if (subcommand === "remove-prompts") {
282
660
  const removed = await removeCodexPromptAliases(customDir);
283
661
  console.log(`Removed ${removed.length} Long Table prompt aliases from ${resolveCodexPromptsDir(customDir)}`);
@@ -335,6 +713,10 @@ async function main() {
335
713
  await runInstall(values);
336
714
  return;
337
715
  }
716
+ if (command === "ask") {
717
+ await runAsk(values);
718
+ return;
719
+ }
338
720
  if (command === "codex") {
339
721
  await runCodexSubcommand(subcommand, values);
340
722
  return;
@@ -346,7 +728,11 @@ async function main() {
346
728
  throw new Error(`Unknown command: ${command}`);
347
729
  }
348
730
  main().catch((error) => {
349
- console.error(error instanceof Error ? error.message : String(error));
731
+ const message = error instanceof Error ? error.message : String(error);
732
+ console.error(message);
733
+ if (message === "Setup cancelled.") {
734
+ exit(1);
735
+ }
350
736
  console.error("");
351
737
  console.error(usage());
352
738
  exit(1);
@@ -1,4 +1,5 @@
1
1
  import { type CanonicalPersona } from "./personas.js";
2
+ import type { InteractionMode } from "@diverga/core";
2
3
  export type OutputLanguage = "ko" | "en";
3
4
  export interface PersonaRoutingResult {
4
5
  outputLanguage: OutputLanguage;
@@ -12,6 +13,7 @@ export declare function parseRoleFlag(value?: string): CanonicalPersona[];
12
13
  export declare function routePersonas(prompt: string, explicitRoleFlag?: string): PersonaRoutingResult;
13
14
  export declare function renderDisclosure(roles: CanonicalPersona[], language: OutputLanguage): string | null;
14
15
  export declare function buildPersonaGuidance(options: {
16
+ mode: InteractionMode;
15
17
  prompt: string;
16
18
  roleFlag?: string;
17
19
  panel?: boolean;
@@ -63,6 +63,9 @@ export function buildPersonaGuidance(options) {
63
63
  const routing = routePersonas(options.prompt, options.roleFlag);
64
64
  const disclosure = renderDisclosure(routing.consultedRoles, routing.outputLanguage);
65
65
  const lines = [];
66
+ lines.push(routing.outputLanguage === "ko"
67
+ ? `Long Table mode: ${options.mode[0].toUpperCase()}${options.mode.slice(1)}`
68
+ : `Long Table mode: ${options.mode[0].toUpperCase()}${options.mode.slice(1)}`);
66
69
  if (disclosure) {
67
70
  lines.push(disclosure);
68
71
  }
@@ -86,6 +89,9 @@ export function buildPersonaGuidance(options) {
86
89
  ? "Include a short deliberation trace showing why the roles diverged."
87
90
  : "Include a short deliberation trace showing why the roles diverged.");
88
91
  }
92
+ lines.push(routing.outputLanguage === "ko"
93
+ ? "Do not show internal file-search logs, tool traces, or process commentary in the researcher-facing answer."
94
+ : "Do not show internal file-search logs, tool traces, or process commentary in the researcher-facing answer.");
89
95
  lines.push(options.prompt.trim());
90
96
  return {
91
97
  guidedPrompt: lines.join("\n\n"),
@@ -7,18 +7,38 @@ export function resolveCodexPromptsDir(customDir) {
7
7
  }
8
8
  function promptSpec() {
9
9
  return [
10
+ {
11
+ name: "longtable",
12
+ description: "Single-entry Long Table router for research conversations",
13
+ argumentHint: "<natural language research request>",
14
+ body: [
15
+ "You are Long Table.",
16
+ "Classify the user's request into one of these modes: explore, review, critique, draft, commit, panel, or status.",
17
+ "If the request is ambiguous, ask one short clarifying question before closing.",
18
+ "Always begin with `Long Table mode: <Mode>`.",
19
+ "Always disclose consulted roles with `Long Table consulted: ...` when any role is foregrounded.",
20
+ "In explore mode, ask at least two clarifying or tension questions before any recommendation.",
21
+ "In panel mode, return 1) Long Table synthesis 2) visible panel opinions by role 3) conflict summary if needed 4) a decision prompt for the researcher.",
22
+ "Do not expose internal tool logs, file searches, or process notes in the researcher-facing answer.",
23
+ "Treat any slash-command arguments as the current research object."
24
+ ]
25
+ },
10
26
  {
11
27
  name: "longtable-init",
12
28
  description: "Run Long Table researcher onboarding inside Codex",
13
29
  argumentHint: "[project context or current uncertainty]",
14
30
  body: [
15
31
  "You are Long Table onboarding inside Codex.",
32
+ "First ask whether the researcher wants Quickstart or Interview setup.",
16
33
  "Ask exactly one setup question at a time.",
17
34
  "Use numbered choices when possible and include a 'None of the above' option when needed.",
18
35
  "Do not move to the next question until the researcher answers the current one.",
19
- "Cover these fields: provider, field, career stage, experience level, current project type, checkpoint intensity, and human authorship signal.",
20
- "After collecting all answers, summarize the proposed setup and tell the researcher the exact `longtable init ...` command to persist it.",
36
+ "Quickstart covers: provider, field, career stage, experience level, current project type, checkpoint intensity, and human authorship signal.",
37
+ "Interview also covers: current research topic, current blocker, preferred entry mode, weakest domain, and panel visibility preference.",
38
+ "After collecting all answers, summarize the proposed setup and then output both: 1) the exact `longtable codex persist-init ... --install-prompts` command and 2) a strict JSON object with keys provider, flow, field, careerStage, experienceLevel, currentProjectType, preferredCheckpointIntensity, and optional humanAuthorshipSignal, currentResearchTopic, currentBlocker, preferredEntryMode, weakestDomain, panelPreference.",
39
+ "If the user prefers paste-based setup, tell them they can pipe the JSON into `longtable codex persist-init --stdin --install-prompts`.",
21
40
  "If the researcher asks you to stay inside Codex, keep the conversation in numbered form and do not prematurely close.",
41
+ "Frame the setup like a short researcher interview, not a bare config form.",
22
42
  "Treat any slash-command arguments as context for why setup is being done now."
23
43
  ]
24
44
  },
@@ -28,9 +48,11 @@ function promptSpec() {
28
48
  argumentHint: "<topic or research problem>",
29
49
  body: [
30
50
  "You are Long Table in explore mode.",
51
+ "Always begin with `Long Table mode: Explore`.",
31
52
  "Ask at least two clarifying or tension questions before any recommendation.",
32
53
  "Keep unresolved tensions visible.",
33
54
  "Do not rush to synthesis.",
55
+ "Do not expose internal process notes or file-search summaries.",
34
56
  "Treat any slash-command arguments as the current research object."
35
57
  ]
36
58
  },
@@ -40,6 +62,7 @@ function promptSpec() {
40
62
  argumentHint: "<claim, paragraph, design, or plan>",
41
63
  body: [
42
64
  "You are Long Table in review mode.",
65
+ "Always begin with `Long Table mode: Review`.",
43
66
  "Surface why this may be wrong before synthesis.",
44
67
  "Preserve the researcher's own language where possible.",
45
68
  "Treat any slash-command arguments as the object to review."
@@ -51,6 +74,7 @@ function promptSpec() {
51
74
  argumentHint: "<claim, plan, or draft for multi-role review>",
52
75
  body: [
53
76
  "You are Long Table in panel mode.",
77
+ "Always begin with `Long Table mode: Panel`.",
54
78
  "Return 1) a Long Table synthesis 2) visible panel opinions by role 3) a decision prompt for the researcher.",
55
79
  "If roles disagree, do not collapse them too early.",
56
80
  "Disclose which roles were consulted.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "description": "Researcher-facing Long Table CLI on top of the legacy Diverga package surface",
6
6
  "type": "module",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@diverga/provider-codex": "0.1.1",
29
- "@diverga/setup": "0.1.1"
29
+ "@diverga/setup": "0.1.2"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.10.1",
@@ -43,9 +43,9 @@
43
43
  "license": "MIT",
44
44
  "repository": {
45
45
  "type": "git",
46
- "url": "git+https://github.com/HosungYou/Diverga-Refactoring.git"
46
+ "url": "git+https://github.com/HosungYou/Long-Table-Refactoring.git"
47
47
  },
48
- "homepage": "https://github.com/HosungYou/Diverga-Refactoring#readme",
48
+ "homepage": "https://github.com/HosungYou/Long-Table-Refactoring#readme",
49
49
  "publishConfig": {
50
50
  "access": "public"
51
51
  },