@longtable/cli 0.1.4 → 0.1.6

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
@@ -53,6 +53,7 @@ longtable start
53
53
  - 프로젝트 디렉토리 생성
54
54
  - `.longtable/` 메모리 파일 생성
55
55
  - 프로젝트용 `AGENTS.md` 생성
56
+ - `START-HERE.md` 생성
56
57
 
57
58
  을 수행합니다.
58
59
 
@@ -71,6 +72,16 @@ codex
71
72
 
72
73
  프로젝트 디렉토리 안에서는 보통 그냥 `codex`를 열고 자연어로 연구를 시작하면 됩니다.
73
74
 
75
+ Codex 안에서 더 명시적으로 부르고 싶다면, 아래처럼 짧은 문법도 사용할 수 있습니다.
76
+
77
+ ```text
78
+ lt explore: 연구 질문을 어디서부터 좁혀야 할지 모르겠어.
79
+ lt review: 이 주장 어디가 약한지 봐줘.
80
+ lt panel: 이 방향에 대한 의견 충돌도 같이 보여줘.
81
+ lt editor: BJET 편집자 관점에서 봐줘.
82
+ lt methods: 방법론적으로 어디가 취약한지 말해줘.
83
+ ```
84
+
74
85
  CLI를 계속 쓰고 싶다면 아래 명령은 보조 경로입니다.
75
86
 
76
87
  ## Advanced commands
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { resolve } from "node:path";
7
7
  import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupAndRuntimeConfig, serializeSetupOutput } from "@diverga/setup";
8
8
  import { buildCodexThinWrappedPrompt, runCodexThinWrapper } from "@diverga/provider-codex";
9
9
  import { installCodexPromptAliases, listInstalledCodexPromptAliases, removeCodexPromptAliases, resolveCodexPromptsDir } from "./prompt-aliases.js";
10
- import { buildPersonaGuidance } from "./persona-router.js";
10
+ import { buildPersonaGuidance, parseInvocationDirective } from "./persona-router.js";
11
11
  import { PERSONA_DEFINITIONS } from "./personas.js";
12
12
  import { createOrUpdateProjectWorkspace, loadProjectContextFromDirectory, renderProjectWorkspaceSummary } from "./project-session.js";
13
13
  const VALID_MODES = new Set([
@@ -27,6 +27,24 @@ const VALID_STAGES = new Set([
27
27
  "writing",
28
28
  "submission"
29
29
  ]);
30
+ const ANSI = {
31
+ reset: "\u001B[0m",
32
+ bold: "\u001B[1m",
33
+ dim: "\u001B[2m",
34
+ cyan: "\u001B[36m",
35
+ green: "\u001B[32m"
36
+ };
37
+ function style(text, prefix) {
38
+ return `${prefix}${text}${ANSI.reset}`;
39
+ }
40
+ function renderSectionCard(title, body) {
41
+ return [
42
+ "┌──────────────────────────────────────────────┐",
43
+ `│ ${title.padEnd(44, " ")}│`,
44
+ "└──────────────────────────────────────────────┘",
45
+ ...body
46
+ ].join("\n");
47
+ }
30
48
  function usage() {
31
49
  return [
32
50
  "Usage:",
@@ -122,15 +140,10 @@ function renderSetupHeader(flow) {
122
140
  const subtitle = flow === "interview"
123
141
  ? "We will ask about your research persona, challenge preferences, and authorship defaults."
124
142
  : "We will capture the minimum profile needed to start using Long Table.";
125
- return [
126
- "┌──────────────────────────────────────────────┐",
127
- `│ ${title.padEnd(44, " ")}│`,
128
- "└──────────────────────────────────────────────┘",
129
- subtitle
130
- ].join("\n");
143
+ return renderSectionCard(title, [subtitle]);
131
144
  }
132
145
  function renderQuestionHeader(index, total, section, prompt) {
133
- return [``, `[${index}/${total}] ${section}`, prompt].join("\n");
146
+ return [``, style(`[${index}/${total}] ${section}`, `${ANSI.bold}${ANSI.cyan}`), prompt].join("\n");
134
147
  }
135
148
  function questionSection(questionId) {
136
149
  if (questionId === "field" || questionId === "careerStage" || questionId === "experienceLevel") {
@@ -154,9 +167,9 @@ function clearLine() {
154
167
  return "\u001B[2K\r";
155
168
  }
156
169
  function renderArrowMenu(prompt, choices, selectedIndex) {
157
- const lines = [prompt, "Use ↑/↓ and Enter."];
170
+ const lines = [style(prompt, ANSI.bold), style("Use ↑/↓ and Enter.", ANSI.dim)];
158
171
  for (let index = 0; index < choices.length; index += 1) {
159
- const prefix = index === selectedIndex ? ">" : " ";
172
+ const prefix = index === selectedIndex ? style(">", `${ANSI.bold}${ANSI.green}`) : " ";
160
173
  lines.push(`${prefix} ${choices[index].label} - ${choices[index].description}`);
161
174
  }
162
175
  return lines.join("\n");
@@ -450,10 +463,10 @@ async function collectProjectInterview(setup, args) {
450
463
  try {
451
464
  if (needsInteractivePrompts) {
452
465
  console.log("");
453
- console.log("┌──────────────────────────────────────────────┐");
454
- console.log(" Long Table Project Start │");
455
- console.log("└──────────────────────────────────────────────┘");
456
- console.log("We will create a project workspace and a session memory seed for today's work.");
466
+ console.log(renderSectionCard("Long Table Project Start", [
467
+ "We will create a project workspace and a session memory seed for today's work.",
468
+ "At the end, Long Table will tell you exactly which directory to open in Codex."
469
+ ]));
457
470
  console.log("");
458
471
  }
459
472
  const projectName = (typeof args.name === "string" && args.name.trim()) ||
@@ -790,7 +803,9 @@ async function runAsk(args) {
790
803
  if (!prompt) {
791
804
  throw new Error("A prompt is required.");
792
805
  }
793
- const inferred = inferModeFromPrompt(prompt);
806
+ const directive = parseInvocationDirective(prompt);
807
+ const effectivePrompt = directive.cleanedPrompt;
808
+ const inferred = directive.mode ?? inferModeFromPrompt(effectivePrompt);
794
809
  if (inferred === "status") {
795
810
  await runCodexSubcommand("status", args);
796
811
  return;
@@ -798,9 +813,12 @@ async function runAsk(args) {
798
813
  const mode = inferred === "panel" ? "review" : inferred;
799
814
  const delegatedArgs = {
800
815
  ...args,
801
- prompt
816
+ prompt: effectivePrompt
802
817
  };
803
- if (inferred === "panel" && delegatedArgs.panel !== true) {
818
+ if (directive.roles.length > 0 && typeof delegatedArgs.role !== "string") {
819
+ delegatedArgs.role = directive.roles.join(",");
820
+ }
821
+ if ((inferred === "panel" || directive.panel) && delegatedArgs.panel !== true) {
804
822
  delegatedArgs.panel = true;
805
823
  delegatedArgs["show-conflicts"] = true;
806
824
  }
@@ -855,11 +873,15 @@ async function runStart(args) {
855
873
  }
856
874
  console.log(renderProjectWorkspaceSummary(context));
857
875
  console.log("");
858
- console.log("Next step:");
859
- console.log(`- cd "${context.project.projectPath}"`);
860
- console.log("- start Codex in that directory if you want a plain Codex session that inherits Long Table project instructions");
861
- console.log(`- or run: longtable ask --cwd "${context.project.projectPath}" --prompt "${context.session.currentGoal.replaceAll("\"", "\\\"")}"`);
862
- console.log("- the workspace now includes `.longtable/` memory files and a project-scoped `AGENTS.md`");
876
+ console.log(renderSectionCard("Next Step", [
877
+ `1. cd "${context.project.projectPath}"`,
878
+ "2. run `codex` in that directory",
879
+ "3. begin with your current goal in natural language",
880
+ "",
881
+ `Suggested first message: ${context.session.currentBlocker ? `"I want to work on ${context.session.currentGoal}. My current blocker is ${context.session.currentBlocker}."` : `"I want to work on ${context.session.currentGoal}."`}`,
882
+ "",
883
+ `Optional CLI path: longtable ask --cwd "${context.project.projectPath}" --prompt "${context.session.currentGoal.replaceAll("\"", "\\\"")}"`
884
+ ]));
863
885
  }
864
886
  async function runCodexSubcommand(subcommand, args) {
865
887
  const customDir = typeof args.dir === "string" ? args.dir : undefined;
@@ -8,8 +8,17 @@ export interface PersonaRoutingResult {
8
8
  consultedRoles: CanonicalPersona[];
9
9
  ambiguousSignal: string | null;
10
10
  }
11
+ export interface LongTableInvocationDirective {
12
+ explicit: boolean;
13
+ cleanedPrompt: string;
14
+ mode?: InteractionMode | "panel" | "status";
15
+ roles: CanonicalPersona[];
16
+ panel: boolean;
17
+ showConflicts: boolean;
18
+ }
11
19
  export declare function detectOutputLanguage(input: string): OutputLanguage;
12
20
  export declare function parseRoleFlag(value?: string): CanonicalPersona[];
21
+ export declare function parseInvocationDirective(prompt: string): LongTableInvocationDirective;
13
22
  export declare function routePersonas(prompt: string, explicitRoleFlag?: string): PersonaRoutingResult;
14
23
  export declare function renderDisclosure(roles: CanonicalPersona[], language: OutputLanguage): string | null;
15
24
  export declare function buildPersonaGuidance(options: {
@@ -1,5 +1,24 @@
1
1
  import { getPersonaDefinition, parsePersonaKey, PERSONA_DEFINITIONS } from "./personas.js";
2
2
  const AUTO_CALL_LIMIT = 3;
3
+ const INVOCATION_PREFIX = /^(?:lt|longtable|long table|롱테이블)\s+/i;
4
+ const DIRECTIVE_MAP = [
5
+ { key: "explore", mode: "explore" },
6
+ { key: "review", mode: "review" },
7
+ { key: "critique", mode: "critique" },
8
+ { key: "draft", mode: "draft" },
9
+ { key: "commit", mode: "commit" },
10
+ { key: "panel", mode: "panel", panel: true, showConflicts: true },
11
+ { key: "status", mode: "status" },
12
+ { key: "editor", mode: "review", roles: ["editor"] },
13
+ { key: "reviewer", mode: "review", roles: ["reviewer"] },
14
+ { key: "methods", mode: "review", roles: ["methods_critic"] },
15
+ { key: "method", mode: "review", roles: ["methods_critic"] },
16
+ { key: "theory", mode: "review", roles: ["theory_critic"] },
17
+ { key: "measurement", mode: "review", roles: ["measurement_auditor"] },
18
+ { key: "ethics", mode: "review", roles: ["ethics_reviewer"] },
19
+ { key: "voice", mode: "review", roles: ["voice_keeper"] },
20
+ { key: "venue", mode: "review", roles: ["venue_strategist"] }
21
+ ];
3
22
  function unique(items) {
4
23
  return [...new Set(items)];
5
24
  }
@@ -15,6 +34,49 @@ export function parseRoleFlag(value) {
15
34
  .map((part) => parsePersonaKey(part))
16
35
  .filter((part) => part !== null));
17
36
  }
37
+ export function parseInvocationDirective(prompt) {
38
+ const trimmed = prompt.trim();
39
+ if (!INVOCATION_PREFIX.test(trimmed)) {
40
+ return {
41
+ explicit: false,
42
+ cleanedPrompt: prompt,
43
+ roles: [],
44
+ panel: false,
45
+ showConflicts: false
46
+ };
47
+ }
48
+ const withoutPrefix = trimmed.replace(INVOCATION_PREFIX, "");
49
+ const colonIndex = withoutPrefix.indexOf(":");
50
+ if (colonIndex === -1) {
51
+ return {
52
+ explicit: false,
53
+ cleanedPrompt: prompt,
54
+ roles: [],
55
+ panel: false,
56
+ showConflicts: false
57
+ };
58
+ }
59
+ const directiveKey = withoutPrefix.slice(0, colonIndex).trim().toLowerCase();
60
+ const body = withoutPrefix.slice(colonIndex + 1).trim();
61
+ const directive = DIRECTIVE_MAP.find((entry) => entry.key === directiveKey);
62
+ if (!directive) {
63
+ return {
64
+ explicit: false,
65
+ cleanedPrompt: body || prompt,
66
+ roles: [],
67
+ panel: false,
68
+ showConflicts: false
69
+ };
70
+ }
71
+ return {
72
+ explicit: true,
73
+ cleanedPrompt: body || prompt,
74
+ mode: directive.mode,
75
+ roles: directive.roles ?? [],
76
+ panel: directive.panel === true,
77
+ showConflicts: directive.showConflicts === true
78
+ };
79
+ }
18
80
  export function routePersonas(prompt, explicitRoleFlag) {
19
81
  const normalizedPrompt = prompt.toLowerCase();
20
82
  const explicitRoles = parseRoleFlag(explicitRoleFlag);
@@ -60,7 +122,10 @@ export function renderDisclosure(roles, language) {
60
122
  : `Long Table consulted: ${labels.join(", ")}`;
61
123
  }
62
124
  export function buildPersonaGuidance(options) {
63
- const routing = routePersonas(options.prompt, options.roleFlag);
125
+ const directive = parseInvocationDirective(options.prompt);
126
+ const effectivePrompt = directive.cleanedPrompt;
127
+ const mergedRoleFlag = [options.roleFlag, directive.roles.join(",")].filter(Boolean).join(",");
128
+ const routing = routePersonas(effectivePrompt, mergedRoleFlag || undefined);
64
129
  const disclosure = renderDisclosure(routing.consultedRoles, routing.outputLanguage);
65
130
  const lines = [];
66
131
  lines.push(routing.outputLanguage === "ko"
@@ -74,12 +139,12 @@ export function buildPersonaGuidance(options) {
74
139
  ? "Ambiguity note: 편집자 관점인지 리뷰어 관점인지 애매합니다. 먼저 둘 중 무엇을 우선할지 짧게 확인하세요."
75
140
  : "Ambiguity note: it is unclear whether the user wants an editor view or reviewer view. Ask briefly before closing.");
76
141
  }
77
- if (options.panel) {
142
+ if (options.panel || directive.panel) {
78
143
  lines.push(routing.outputLanguage === "ko"
79
144
  ? "Return format: 1) Long Table synthesis 2) panel opinions by role 3) decision prompt to the researcher."
80
145
  : "Return format: 1) Long Table synthesis 2) panel opinions by role 3) decision prompt to the researcher.");
81
146
  }
82
- if (options.showConflicts) {
147
+ if (options.showConflicts || directive.showConflicts) {
83
148
  lines.push(routing.outputLanguage === "ko"
84
149
  ? "If roles disagree, show the conflict explicitly instead of forcing one answer."
85
150
  : "If roles disagree, show the conflict explicitly instead of forcing one answer.");
@@ -92,7 +157,7 @@ export function buildPersonaGuidance(options) {
92
157
  lines.push(routing.outputLanguage === "ko"
93
158
  ? "Do not show internal file-search logs, tool traces, or process commentary in the researcher-facing answer."
94
159
  : "Do not show internal file-search logs, tool traces, or process commentary in the researcher-facing answer.");
95
- lines.push(options.prompt.trim());
160
+ lines.push(effectivePrompt.trim());
96
161
  return {
97
162
  guidedPrompt: lines.join("\n\n"),
98
163
  routing
@@ -42,6 +42,46 @@ function buildWorkspaceGuide(project, session) {
42
42
  ];
43
43
  return lines.join("\n");
44
44
  }
45
+ function buildStartHereGuide(project, session) {
46
+ const lines = [
47
+ "# Start Here",
48
+ "",
49
+ `Project: ${project.projectName}`,
50
+ "",
51
+ "This workspace was created by Long Table for a single research project.",
52
+ "",
53
+ "## What to do now",
54
+ "1. Open Codex in this directory.",
55
+ "2. Start with your current goal, not a shell command.",
56
+ "3. Let Long Table keep disagreement visible when it matters.",
57
+ "",
58
+ "## Suggested first message",
59
+ session.currentBlocker
60
+ ? `"I want to work on ${session.currentGoal}. My current blocker is: ${session.currentBlocker}."`
61
+ : `"I want to work on ${session.currentGoal}."`,
62
+ "",
63
+ "## Explicit Long Table invocation inside Codex",
64
+ "- `lt explore: <question>`",
65
+ "- `lt review: <claim or plan>`",
66
+ "- `lt panel: <claim or plan>`",
67
+ "- `lt editor: <draft or positioning question>`",
68
+ "- `lt reviewer: <claim or section>`",
69
+ "- `lt methods: <design, measure, or analysis plan>`",
70
+ "- `lt commit: <decision that needs commitment>`",
71
+ "",
72
+ "## What Long Table already knows in this directory",
73
+ `- Current goal: ${session.currentGoal}`,
74
+ ...(session.currentBlocker ? [`- Current blocker: ${session.currentBlocker}`] : []),
75
+ `- Requested perspectives: ${session.requestedPerspectives.length > 0 ? session.requestedPerspectives.join(", ") : "auto"}`,
76
+ `- Disagreement visibility: ${session.disagreementPreference}`,
77
+ "",
78
+ "## Files",
79
+ "- `AGENTS.md` tells Codex how to behave in this workspace.",
80
+ "- `.longtable/current-session.json` stores the current session goal and blocker.",
81
+ "- `.longtable/project.json` stores the project-level record."
82
+ ];
83
+ return lines.join("\n");
84
+ }
45
85
  function buildProjectAgentsMd(project, session) {
46
86
  return [
47
87
  "# AGENTS.md",
@@ -57,15 +97,23 @@ function buildProjectAgentsMd(project, session) {
57
97
  "",
58
98
  "## Research-facing behavior",
59
99
  "- Treat researcher interaction as the primary task.",
100
+ "- If the user message starts with `lt `, `longtable `, `long table `, or `롱테이블 ` followed by a directive and `:`, treat it as an explicit Long Table invocation.",
101
+ "- Supported explicit directives are: explore, review, critique, draft, commit, panel, status, editor, reviewer, methods, theory, measurement, ethics, voice, venue.",
102
+ "- For explicit Long Table invocations, do not begin by scanning the workspace. Use the current session files first and answer as Long Table immediately.",
103
+ "- For general research requests in this workspace, prefer Long Table behavior before generic coding behavior.",
60
104
  "- Begin exploratory work with clarifying or tension questions before recommending a direction.",
61
105
  "- If you foreground role perspectives, disclose them with `Long Table consulted: ...`.",
62
106
  "- Keep one accountable synthesis, but do not hide meaningful disagreement.",
107
+ ...(session.disagreementPreference === "always_visible"
108
+ ? ["- In this workspace, panel disagreement should be visible by default rather than hidden behind a single synthesis."]
109
+ : []),
63
110
  "- Do not expose internal tool logs, file-search traces, or process commentary in the researcher-facing answer.",
64
111
  "",
65
112
  "## Session memory",
66
113
  "- Read `.longtable/current-session.json` before giving substantial guidance.",
67
114
  "- Use `.longtable/project.json` as the project-level context.",
68
115
  "- Prefer the current goal and blocker over generic assumptions.",
116
+ "- Do not recursively search unrelated files unless the researcher explicitly asks for file analysis or code changes.",
69
117
  "",
70
118
  "## Scope",
71
119
  "- These instructions apply to this directory and its children."
@@ -161,6 +209,7 @@ export async function createOrUpdateProjectWorkspace(options) {
161
209
  await writeFile(join(sessionsDir, `${sessionId}.json`), JSON.stringify(session, null, 2), "utf8");
162
210
  await writeFile(join(metaDir, "state.json"), buildStateSeed(project, session, options.setup), "utf8");
163
211
  await writeFile(join(projectPath, "LONGTABLE.md"), buildWorkspaceGuide(project, session), "utf8");
212
+ await writeFile(join(projectPath, "START-HERE.md"), buildStartHereGuide(project, session), "utf8");
164
213
  await writeFile(join(projectPath, "AGENTS.md"), buildProjectAgentsMd(project, session), "utf8");
165
214
  return {
166
215
  project,
@@ -194,14 +243,20 @@ export async function loadProjectContextFromDirectory(startPath) {
194
243
  }
195
244
  export function renderProjectWorkspaceSummary(context) {
196
245
  return [
197
- "Long Table project workspace",
198
- `project: ${context.project.projectName}`,
199
- `path: ${context.project.projectPath}`,
200
- `goal: ${context.session.currentGoal}`,
201
- ...(context.session.currentBlocker ? [`blocker: ${context.session.currentBlocker}`] : []),
202
- `perspectives: ${context.session.requestedPerspectives.length > 0 ? context.session.requestedPerspectives.join(", ") : "auto"}`,
203
- `disagreement: ${context.session.disagreementPreference}`,
204
- `project file: ${context.projectFilePath}`,
205
- `session file: ${context.sessionFilePath}`
246
+ "┌──────────────────────────────────────────────┐",
247
+ "│ Long Table Project Workspace │",
248
+ "└──────────────────────────────────────────────┘",
249
+ `Project: ${context.project.projectName}`,
250
+ `Path: ${context.project.projectPath}`,
251
+ `Goal: ${context.session.currentGoal}`,
252
+ ...(context.session.currentBlocker ? [`Blocker: ${context.session.currentBlocker}`] : []),
253
+ `Perspectives: ${context.session.requestedPerspectives.length > 0 ? context.session.requestedPerspectives.join(", ") : "auto"}`,
254
+ `Disagreement: ${context.session.disagreementPreference}`,
255
+ "",
256
+ "Created files:",
257
+ `- ${context.projectFilePath}`,
258
+ `- ${context.sessionFilePath}`,
259
+ `- ${join(context.project.projectPath, "START-HERE.md")}`,
260
+ `- ${join(context.project.projectPath, "AGENTS.md")}`
206
261
  ].join("\n");
207
262
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longtable/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "description": "Researcher-facing Long Table CLI on top of the legacy Diverga package surface",
6
6
  "type": "module",