@longtable/cli 0.1.30 → 0.1.31

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.
@@ -1,6 +1,24 @@
1
1
  import type { DecisionRecord, InvocationRecord, ProviderKind, QuestionOption, 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
+ export type StartInterviewSignal = "phenomenon" | "audience" | "artifact" | "evidence" | "assumption" | "decision_risk" | "voice";
5
+ export interface StartInterviewTurn {
6
+ index: number;
7
+ question: string;
8
+ answer: string;
9
+ signal: StartInterviewSignal;
10
+ purpose: string;
11
+ }
12
+ export interface StartInterviewSession {
13
+ mode: "adaptive";
14
+ openingStyle: "scene_problem";
15
+ createdAt: string;
16
+ completedAt: string;
17
+ turnCount: number;
18
+ turns: StartInterviewTurn[];
19
+ inferredSignals: StartInterviewSignal[];
20
+ summary: string;
21
+ }
4
22
  export interface LongTableProjectRecord {
5
23
  schemaVersion: 1;
6
24
  product: "LongTable";
@@ -33,6 +51,7 @@ export interface LongTableSessionRecord {
33
51
  protectedDecision?: string;
34
52
  nextAction?: string;
35
53
  openQuestions?: string[];
54
+ startInterview?: StartInterviewSession;
36
55
  requestedPerspectives: string[];
37
56
  disagreementPreference: ProjectDisagreementPreference;
38
57
  activeModes?: string[];
@@ -159,6 +178,7 @@ export declare function createOrUpdateProjectWorkspace(options: {
159
178
  researchObject?: string;
160
179
  gapRisk?: string;
161
180
  protectedDecision?: string;
181
+ startInterview?: StartInterviewSession;
162
182
  requestedPerspectives: string[];
163
183
  disagreementPreference: ProjectDisagreementPreference;
164
184
  setup: SetupPersistedOutput;
@@ -55,15 +55,21 @@ function resolveUserLocale() {
55
55
  }
56
56
  function buildFirstQuestion(session) {
57
57
  return session.currentBlocker
58
- ? `What would reduce the uncertainty around "${session.currentBlocker}" first?`
59
- : `What is the first concrete question that would move "${session.currentGoal}" forward?`;
58
+ ? `Where does "${session.currentBlocker}" show up most concretely in the scene, material, or evidence?`
59
+ : `What scene, case, text, data, or draft would make "${session.currentGoal}" easiest to inspect first?`;
60
60
  }
61
61
  function buildOpenQuestions(session) {
62
62
  const firstQuestion = buildFirstQuestion(session);
63
+ if (session.startInterview) {
64
+ return [
65
+ firstQuestion,
66
+ `What would a reader need to understand differently if "${session.currentGoal}" becomes a strong research project?`
67
+ ];
68
+ }
63
69
  return session.currentBlocker
64
70
  ? [
65
71
  firstQuestion,
66
- `What evidence would let you decide whether "${session.currentBlocker}" is a knowledge gap, a coding rule gap, or a data gap?`
72
+ `What would a reader need to understand differently if "${session.currentBlocker}" becomes a strong research problem?`
67
73
  ]
68
74
  : [
69
75
  firstQuestion,
@@ -71,11 +77,21 @@ function buildOpenQuestions(session) {
71
77
  ];
72
78
  }
73
79
  function buildNextAction(session) {
80
+ if (session.startInterview) {
81
+ return session.currentBlocker
82
+ ? `Begin from the start-interview brief, then make "${session.currentBlocker}" concrete with one scene, source, case, or dataset.`
83
+ : "Begin from the start-interview brief, then choose one concrete scene, source, case, or dataset to inspect first.";
84
+ }
74
85
  return session.currentBlocker
75
- ? `Open with the blocker, then ask LongTable to surface the first high-leverage uncertainty around "${session.currentBlocker}".`
86
+ ? `Open with the blocker, then make "${session.currentBlocker}" concrete with one scene, source, case, or dataset.`
76
87
  : "Open with your current goal in one sentence, then ask LongTable for the first concrete research move.";
77
88
  }
78
89
  function buildResumeHint(session) {
90
+ if (session.startInterview) {
91
+ return session.currentBlocker
92
+ ? `I want to continue from the LongTable start interview. The first unresolved issue is ${session.currentBlocker}.`
93
+ : `I want to continue from the LongTable start interview for ${session.currentGoal}.`;
94
+ }
79
95
  return session.currentBlocker
80
96
  ? `I want to continue ${session.currentGoal}. The unresolved blocker is ${session.currentBlocker}.`
81
97
  : `I want to continue ${session.currentGoal}.`;
@@ -102,6 +118,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
102
118
  ...(session.researchObject ? [`- 연구 객체: ${session.researchObject}`] : []),
103
119
  ...(session.gapRisk ? [`- 공백/암묵지 위험: ${session.gapRisk}`] : []),
104
120
  ...(session.protectedDecision ? [`- 보호할 결정: ${session.protectedDecision}`] : []),
121
+ ...(session.startInterview ? [`- start interview: ${session.startInterview.summary}`] : []),
105
122
  `- 다음 액션: ${nextAction}`,
106
123
  `- 관점: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
107
124
  `- disagreement: ${session.disagreementPreference}`,
@@ -154,6 +171,7 @@ function buildCurrentGuide(project, session, recentInvocations = [], pendingQues
154
171
  ...(session.researchObject ? [`- Research object: ${session.researchObject}`] : []),
155
172
  ...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
156
173
  ...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
174
+ ...(session.startInterview ? [`- Start interview: ${session.startInterview.summary}`] : []),
157
175
  `- Next action: ${nextAction}`,
158
176
  `- Perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
159
177
  `- Disagreement: ${session.disagreementPreference}`,
@@ -344,6 +362,7 @@ function buildProjectAgentsMd(project, session) {
344
362
  ...(session.researchObject ? [`- Research object: ${session.researchObject}`] : []),
345
363
  ...(session.gapRisk ? [`- Gap/tacit risk: ${session.gapRisk}`] : []),
346
364
  ...(session.protectedDecision ? [`- Protected decision: ${session.protectedDecision}`] : []),
365
+ ...(session.startInterview ? [`- Start interview summary: ${session.startInterview.summary}`] : []),
347
366
  `- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
348
367
  `- Disagreement visibility: ${session.disagreementPreference}`,
349
368
  "- These instructions apply to this directory and its children."
@@ -368,6 +387,7 @@ function buildStateSeed(project, session, setup) {
368
387
  ...(session.nextAction ? { nextAction: session.nextAction } : {}),
369
388
  openQuestions: session.openQuestions ?? [],
370
389
  activeModes: session.activeModes ?? [],
390
+ ...(session.startInterview ? { startInterview: session.startInterview } : {}),
371
391
  ...(session.resumeHint ? { resumeHint: session.resumeHint } : {})
372
392
  };
373
393
  if (session.currentBlocker) {
@@ -414,6 +434,25 @@ function buildStateSeed(project, session, setup) {
414
434
  importance: "high"
415
435
  });
416
436
  }
437
+ if (session.startInterview) {
438
+ state.narrativeTraces.push({
439
+ id: "project-session-start-interview",
440
+ timestamp: nowIso(),
441
+ source: "longtable-start",
442
+ traceType: "experience",
443
+ summary: session.startInterview.summary,
444
+ visibility: "explicit",
445
+ importance: "high"
446
+ });
447
+ if (session.startInterview.inferredSignals.length > 0) {
448
+ state.inferredHypotheses.push({
449
+ hypothesis: `Start interview suggests these early lenses: ${session.startInterview.inferredSignals.join(", ")}.`,
450
+ confidence: 0.65,
451
+ evidence: session.startInterview.turns.map((turn) => turn.answer).filter(Boolean),
452
+ status: "unconfirmed"
453
+ });
454
+ }
455
+ }
417
456
  return JSON.stringify(state, null, 2);
418
457
  }
419
458
  async function removeLegacyRootFiles(projectPath) {
@@ -1008,6 +1047,7 @@ export async function createOrUpdateProjectWorkspace(options) {
1008
1047
  ...(options.researchObject ? { researchObject: options.researchObject } : {}),
1009
1048
  ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
1010
1049
  ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
1050
+ ...(options.startInterview ? { startInterview: options.startInterview } : {}),
1011
1051
  nextAction: buildNextAction({
1012
1052
  schemaVersion: 1,
1013
1053
  id: sessionId,
@@ -1019,6 +1059,7 @@ export async function createOrUpdateProjectWorkspace(options) {
1019
1059
  ...(options.researchObject ? { researchObject: options.researchObject } : {}),
1020
1060
  ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
1021
1061
  ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
1062
+ ...(options.startInterview ? { startInterview: options.startInterview } : {}),
1022
1063
  requestedPerspectives: options.requestedPerspectives,
1023
1064
  disagreementPreference: options.disagreementPreference
1024
1065
  }),
@@ -1033,6 +1074,7 @@ export async function createOrUpdateProjectWorkspace(options) {
1033
1074
  ...(options.researchObject ? { researchObject: options.researchObject } : {}),
1034
1075
  ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
1035
1076
  ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
1077
+ ...(options.startInterview ? { startInterview: options.startInterview } : {}),
1036
1078
  requestedPerspectives: options.requestedPerspectives,
1037
1079
  disagreementPreference: options.disagreementPreference
1038
1080
  }),
@@ -1050,6 +1092,7 @@ export async function createOrUpdateProjectWorkspace(options) {
1050
1092
  ...(options.researchObject ? { researchObject: options.researchObject } : {}),
1051
1093
  ...(options.gapRisk ? { gapRisk: options.gapRisk } : {}),
1052
1094
  ...(options.protectedDecision ? { protectedDecision: options.protectedDecision } : {}),
1095
+ ...(options.startInterview ? { startInterview: options.startInterview } : {}),
1053
1096
  requestedPerspectives: options.requestedPerspectives,
1054
1097
  disagreementPreference: options.disagreementPreference
1055
1098
  }),
@@ -1123,13 +1166,18 @@ export async function inspectProjectWorkspace(startPath) {
1123
1166
  }
1124
1167
  export function renderProjectWorkspaceSummary(context) {
1125
1168
  return [
1126
- "┌──────────────────────────────────────────────┐",
1127
- "│ LongTable Project Workspace │",
1128
- "└──────────────────────────────────────────────┘",
1169
+ "┌─ LongTable Project Workspace ─────────────────────────┐",
1170
+ "└───────────────────────────────────────────────────────┘",
1129
1171
  `Project: ${context.project.projectName}`,
1130
1172
  `Path: ${context.project.projectPath}`,
1173
+ "",
1174
+ "┌─ Current Research Shape ──────────────────────────────┐",
1131
1175
  `Goal: ${context.session.currentGoal}`,
1132
1176
  ...(context.session.currentBlocker ? [`Blocker: ${context.session.currentBlocker}`] : []),
1177
+ ...(context.session.researchObject ? [`Working object: ${context.session.researchObject}`] : []),
1178
+ ...(context.session.startInterview ? [`Start interview: ${context.session.startInterview.summary}`] : []),
1179
+ "└───────────────────────────────────────────────────────┘",
1180
+ "",
1133
1181
  `Perspectives: ${context.session.requestedPerspectives.length > 0 ? context.session.requestedPerspectives.join(", ") : "auto"}`,
1134
1182
  `Disagreement: ${context.session.disagreementPreference}`,
1135
1183
  "",
@@ -0,0 +1,11 @@
1
+ import type { SetupChoice } from "@longtable/setup";
2
+ export interface PromptRenderer {
3
+ text(prompt: string, options?: {
4
+ required?: boolean;
5
+ placeholder?: string;
6
+ }): Promise<string | undefined>;
7
+ select(prompt: string, choices: SetupChoice[]): Promise<string>;
8
+ multiselect(prompt: string, choices: SetupChoice[]): Promise<string[]>;
9
+ note(message: string, title?: string): void;
10
+ }
11
+ export declare function createPromptRenderer(): PromptRenderer;
@@ -0,0 +1,130 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin as input, stdout as output } from "node:process";
4
+ import { cancel, isCancel, multiselect, note, select, text } from "@clack/prompts";
5
+ function isInteractiveTerminal() {
6
+ return Boolean(input.isTTY && output.isTTY);
7
+ }
8
+ function guardCancel(value) {
9
+ if (isCancel(value)) {
10
+ cancel("LongTable cancelled.");
11
+ throw new Error("LongTable cancelled.");
12
+ }
13
+ return value;
14
+ }
15
+ function renderChoices(choices) {
16
+ return choices
17
+ .map((choice, index) => `${index + 1}. ${choice.label} - ${choice.description}`)
18
+ .join("\n");
19
+ }
20
+ let fallbackReadline;
21
+ let fallbackInputLines;
22
+ function getFallbackReadline() {
23
+ fallbackReadline ??= createInterface({ input, output });
24
+ return fallbackReadline;
25
+ }
26
+ async function askLine(prompt) {
27
+ if (!input.isTTY) {
28
+ output.write(prompt);
29
+ fallbackInputLines ??= readFileSync(0, "utf8").split(/\r?\n/);
30
+ return fallbackInputLines.shift() ?? "";
31
+ }
32
+ return getFallbackReadline().question(prompt);
33
+ }
34
+ async function promptChoiceByNumber(prompt, choices) {
35
+ while (true) {
36
+ const answer = await askLine(`${prompt}\n${renderChoices(choices)}\nSelect one number: `);
37
+ const numeric = Number(answer.trim());
38
+ if (!Number.isInteger(numeric) || numeric < 1 || numeric > choices.length) {
39
+ console.log("Invalid selection. Enter one of the listed numbers.");
40
+ continue;
41
+ }
42
+ const choice = choices[numeric - 1];
43
+ if (choice.fallbackToText) {
44
+ const freeText = await askLine("Type your custom value: ");
45
+ if (!freeText.trim()) {
46
+ console.log("Custom value cannot be empty.");
47
+ continue;
48
+ }
49
+ return freeText.trim();
50
+ }
51
+ return choice.id;
52
+ }
53
+ }
54
+ async function promptTextLine(prompt, required) {
55
+ while (true) {
56
+ const answer = (await askLine(`${prompt}\n> `)).trim();
57
+ if (!required) {
58
+ return answer || undefined;
59
+ }
60
+ if (answer) {
61
+ return answer;
62
+ }
63
+ console.log("This answer cannot be empty.");
64
+ }
65
+ }
66
+ export function createPromptRenderer() {
67
+ return {
68
+ async text(prompt, options) {
69
+ const required = options?.required ?? false;
70
+ if (!isInteractiveTerminal()) {
71
+ return promptTextLine(prompt, required);
72
+ }
73
+ return guardCancel(await text({
74
+ message: prompt,
75
+ placeholder: options?.placeholder,
76
+ validate: required
77
+ ? (value) => value?.trim() ? undefined : "This answer cannot be empty."
78
+ : undefined
79
+ }))?.trim() || undefined;
80
+ },
81
+ async select(prompt, choices) {
82
+ if (!isInteractiveTerminal()) {
83
+ return promptChoiceByNumber(prompt, choices);
84
+ }
85
+ const selected = guardCancel(await select({
86
+ message: prompt,
87
+ options: choices.map((choice) => ({
88
+ value: choice.id,
89
+ label: choice.label,
90
+ hint: choice.description
91
+ })),
92
+ maxItems: 7
93
+ }));
94
+ const choice = choices.find((candidate) => candidate.id === selected);
95
+ if (choice?.fallbackToText) {
96
+ return guardCancel(await text({
97
+ message: "Type your custom value",
98
+ validate: (value) => value?.trim() ? undefined : "This answer cannot be empty."
99
+ })).trim();
100
+ }
101
+ return selected;
102
+ },
103
+ async multiselect(prompt, choices) {
104
+ if (!isInteractiveTerminal()) {
105
+ const answer = await askLine(`${prompt}\nType comma-separated ids or leave blank for auto.\n> `);
106
+ return answer
107
+ .split(",")
108
+ .map((part) => part.trim())
109
+ .filter(Boolean);
110
+ }
111
+ return guardCancel(await multiselect({
112
+ message: prompt,
113
+ options: choices.map((choice) => ({
114
+ value: choice.id,
115
+ label: choice.label,
116
+ hint: choice.description
117
+ })),
118
+ required: false,
119
+ maxItems: 7
120
+ }));
121
+ },
122
+ note(message, title) {
123
+ if (isInteractiveTerminal()) {
124
+ note(message, title);
125
+ return;
126
+ }
127
+ console.log(title ? `${title}\n${message}` : message);
128
+ }
129
+ };
130
+ }
@@ -151,7 +151,7 @@ export async function discoverCrossrefTdm(doi, env = process.env, httpFetch = de
151
151
  const response = await httpFetch(url, {
152
152
  headers: {
153
153
  accept: "application/json",
154
- "user-agent": "LongTable/0.1.30 (https://github.com/HosungYou/LongTable)"
154
+ "user-agent": "LongTable/0.1.31 (https://github.com/HosungYou/LongTable)"
155
155
  }
156
156
  });
157
157
  if (!response.ok) {
@@ -155,7 +155,7 @@ async function fetchJson(context, url) {
155
155
  const response = await context.fetch(url, {
156
156
  headers: {
157
157
  "accept": "application/json",
158
- "user-agent": "LongTable/0.1.30 (https://github.com/HosungYou/LongTable)"
158
+ "user-agent": "LongTable/0.1.31 (https://github.com/HosungYou/LongTable)"
159
159
  }
160
160
  });
161
161
  if (!response.ok) {
@@ -167,7 +167,7 @@ async function fetchText(context, url) {
167
167
  const response = await context.fetch(url, {
168
168
  headers: {
169
169
  "accept": "application/xml, text/xml, application/atom+xml, text/plain",
170
- "user-agent": "LongTable/0.1.30 (https://github.com/HosungYou/LongTable)"
170
+ "user-agent": "LongTable/0.1.31 (https://github.com/HosungYou/LongTable)"
171
171
  }
172
172
  });
173
173
  if (!response.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "private": false,
5
5
  "description": "Researcher-facing LongTable CLI",
6
6
  "type": "module",
@@ -28,12 +28,13 @@
28
28
  "typecheck": "tsc -p tsconfig.json --noEmit"
29
29
  },
30
30
  "dependencies": {
31
- "@longtable/checkpoints": "0.1.30",
32
- "@longtable/core": "0.1.30",
33
- "@longtable/memory": "0.1.30",
34
- "@longtable/provider-claude": "0.1.30",
35
- "@longtable/provider-codex": "0.1.30",
36
- "@longtable/setup": "0.1.30"
31
+ "@clack/prompts": "^1.2.0",
32
+ "@longtable/checkpoints": "0.1.31",
33
+ "@longtable/core": "0.1.31",
34
+ "@longtable/memory": "0.1.31",
35
+ "@longtable/provider-claude": "0.1.31",
36
+ "@longtable/provider-codex": "0.1.31",
37
+ "@longtable/setup": "0.1.31"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/node": "^22.10.1",