@longtable/setup 0.1.9

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/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## 0.1.4
4
+
5
+ - treat global setup as researcher-profile onboarding, not project intake
6
+ - remove project-specific defaults from setup examples and persisted summaries
7
+ - align setup output with the new `longtable start` project interview flow
8
+
9
+ ## 0.1.3
10
+
11
+ - make setup prompts read more like a short researcher interview and less like a raw config form
12
+
13
+ ## 0.1.2
14
+
15
+ - add `quickstart` and `interview` setup flows
16
+ - persist topic, blocker, preferred entry mode, weakest domain, and panel preference
17
+ - enrich runtime artifacts so setup can connect directly to the first research question
18
+
19
+ ## 0.1.1
20
+
21
+ - switch CLI packaging to `directories.bin` so npm preserves the published executable
22
+ - keep the executable name as `longtable-setup`
23
+
24
+ ## 0.1.0
25
+
26
+ - initial publish-ready setup package scaffold
27
+ - quick setup flow and provider selection
28
+ - persisted setup output generation
29
+ - CLI entrypoint for setup initialization
30
+ - example setup outputs for Codex and Claude
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @longtable/setup
2
+
3
+ Researcher onboarding and setup flows for LongTable.
4
+
5
+ ## Recommended Usage
6
+
7
+ Researchers should use the unified CLI:
8
+
9
+ ```bash
10
+ npm install -g @longtable/cli
11
+ longtable init --flow interview
12
+ ```
13
+
14
+ This package exists so the setup flow can also be consumed programmatically or tested in isolation during development.
15
+
16
+ By default this writes:
17
+
18
+ - setup output to `~/.longtable/setup.json`
19
+ - Codex runtime config to `~/.longtable/runtime/codex/longtable.toml`
20
+ - Claude runtime config to `~/.longtable/runtime/claude/longtable.json`
21
+
22
+ The generated runtime config does not overwrite platform-native config files directly. It creates LongTable-managed runtime artifacts that can later be wired into provider-specific runtimes during migration.
23
+
24
+ ## Package Role
25
+
26
+ The setup contract combines technical setup with researcher-profile calibration rather than treating installation as a purely technical step.
27
+
28
+ Global setup should answer:
29
+
30
+ - who the researcher is
31
+ - how much challenge or slowdown they want
32
+ - what makes writing still feel like theirs
33
+ - how visible disagreement should be by default
34
+
35
+ Project and session intake belongs to `longtable start`, not library-level setup helpers.
36
+
37
+ ## Included Outputs
38
+
39
+ - quick setup question flow
40
+ - provider selection resolution
41
+ - persisted setup output generator
42
+ - saved setup output helpers
43
+ - runtime config installer helpers
44
+ - numbered checkpoint helpers
45
+
46
+ See `examples/` for sample Codex and Claude setup outputs.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,275 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput } from "./onboarding.js";
4
+ import { installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultSetupPath, saveSetupAndRuntimeConfig, saveSetupOutput, serializeSetupOutput } from "./persistence.js";
5
+ function printUsage() {
6
+ console.log(`Usage:
7
+ longtable-setup init [--flow quickstart|interview] --provider <codex|claude> --field <field> --career-stage <stage> --experience <novice|intermediate|advanced> --checkpoint <low|balanced|high> [--authorship-signal <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]
8
+ longtable-setup install [--path <file>] [--runtime-path <file>] [--json]
9
+ longtable-setup show [--path <file>] [--json]
10
+
11
+ Example:
12
+ longtable-setup init --flow interview --provider codex --field education --career-stage doctoral --experience intermediate --checkpoint balanced --entry-mode explore --panel-preference show_on_conflict --json
13
+
14
+ If required flags are omitted, interactive setup starts automatically.
15
+ Use --write to save setup.json and --install to also generate provider runtime config.`);
16
+ }
17
+ function parseArgs(argv) {
18
+ const parsed = {};
19
+ for (let index = 0; index < argv.length; index += 1) {
20
+ const token = argv[index];
21
+ if (!token.startsWith("--")) {
22
+ parsed._command = token;
23
+ continue;
24
+ }
25
+ const key = token.slice(2);
26
+ const next = argv[index + 1];
27
+ if (!next || next.startsWith("--")) {
28
+ parsed[key] = true;
29
+ continue;
30
+ }
31
+ parsed[key] = next;
32
+ index += 1;
33
+ }
34
+ return parsed;
35
+ }
36
+ function requireString(args, key) {
37
+ const value = args[key];
38
+ if (typeof value !== "string" || value.trim().length === 0) {
39
+ throw new Error(`Missing required argument: --${key}`);
40
+ }
41
+ return value;
42
+ }
43
+ function toSetupAnswers(args) {
44
+ return {
45
+ field: requireString(args, "field"),
46
+ careerStage: requireString(args, "career-stage"),
47
+ experienceLevel: requireString(args, "experience"),
48
+ preferredCheckpointIntensity: requireString(args, "checkpoint"),
49
+ humanAuthorshipSignal: typeof args["authorship-signal"] === "string" && args["authorship-signal"].trim().length > 0
50
+ ? args["authorship-signal"].trim()
51
+ : undefined,
52
+ preferredEntryMode: typeof args["entry-mode"] === "string" && args["entry-mode"].trim().length > 0
53
+ ? args["entry-mode"].trim()
54
+ : undefined,
55
+ weakestDomain: typeof args["weakest-domain"] === "string" && args["weakest-domain"].trim().length > 0
56
+ ? args["weakest-domain"].trim()
57
+ : undefined,
58
+ panelPreference: typeof args["panel-preference"] === "string" && args["panel-preference"].trim().length > 0
59
+ ? args["panel-preference"].trim()
60
+ : undefined
61
+ };
62
+ }
63
+ function hasCompleteFlagInput(args) {
64
+ const requiredKeys = [
65
+ "provider",
66
+ "field",
67
+ "career-stage",
68
+ "experience",
69
+ "checkpoint"
70
+ ];
71
+ return requiredKeys.every((key) => typeof args[key] === "string" && String(args[key]).trim().length > 0);
72
+ }
73
+ function resolveSetupFlow(args) {
74
+ return args.flow === "interview" ? "interview" : "quickstart";
75
+ }
76
+ function renderChoices(choices) {
77
+ return choices
78
+ .map((choice, index) => `${index + 1}. ${choice.label} — ${choice.description}`)
79
+ .join("\n");
80
+ }
81
+ async function readPipedLines() {
82
+ const chunks = [];
83
+ for await (const chunk of input) {
84
+ chunks.push(String(chunk));
85
+ }
86
+ return chunks
87
+ .join("")
88
+ .split(/\r?\n/);
89
+ }
90
+ async function promptChoice(rl, prompt, choices) {
91
+ while (true) {
92
+ const answer = await rl.question(`${prompt}\n${renderChoices(choices)}\nSelect one number: `);
93
+ const numeric = Number(answer.trim());
94
+ if (!Number.isInteger(numeric) || numeric < 1 || numeric > choices.length) {
95
+ console.log("Invalid selection. Enter one of the listed numbers.");
96
+ continue;
97
+ }
98
+ const choice = choices[numeric - 1];
99
+ if (choice.fallbackToText) {
100
+ const freeText = await rl.question("Type your custom value: ");
101
+ if (freeText.trim().length === 0) {
102
+ console.log("Custom value cannot be empty.");
103
+ continue;
104
+ }
105
+ return freeText.trim();
106
+ }
107
+ return choice.id;
108
+ }
109
+ }
110
+ function promptChoiceFromLines(prompt, choices, lines, state) {
111
+ while (true) {
112
+ console.log(`${prompt}\n${renderChoices(choices)}\nSelect one number: `);
113
+ const answer = lines[state.index] ?? "";
114
+ state.index += 1;
115
+ const numeric = Number(answer.trim());
116
+ if (!Number.isInteger(numeric) || numeric < 1 || numeric > choices.length) {
117
+ console.log("Invalid selection. Enter one of the listed numbers.");
118
+ continue;
119
+ }
120
+ const choice = choices[numeric - 1];
121
+ if (choice.fallbackToText) {
122
+ console.log("Type your custom value: ");
123
+ const freeText = (lines[state.index] ?? "").trim();
124
+ state.index += 1;
125
+ if (freeText.length === 0) {
126
+ console.log("Custom value cannot be empty.");
127
+ continue;
128
+ }
129
+ return freeText;
130
+ }
131
+ return choice.id;
132
+ }
133
+ }
134
+ function printInitResult(outputValue, options, installResult) {
135
+ if (options.writeOutput && installResult) {
136
+ console.error(`Saved setup output to ${installResult.setupTarget.path}`);
137
+ console.error(`Installed runtime config to ${installResult.runtimeTarget.path}`);
138
+ }
139
+ else if (options.writeOutput) {
140
+ const target = resolveDefaultSetupPath(options.customPath);
141
+ console.error(`Saved setup output to ${target.path}`);
142
+ }
143
+ if (options.jsonOutput) {
144
+ console.log(serializeSetupOutput(outputValue));
145
+ return;
146
+ }
147
+ const summary = [renderSetupSummary(outputValue)];
148
+ if (installResult) {
149
+ summary.push("");
150
+ summary.push(renderInstallSummary(installResult));
151
+ }
152
+ console.log(summary.join("\n"));
153
+ }
154
+ async function persistInitResult(outputValue, options) {
155
+ let installResult;
156
+ if (options.installRuntime) {
157
+ installResult = await saveSetupAndRuntimeConfig(outputValue, {
158
+ setupPath: options.customPath,
159
+ runtimePath: options.runtimePath
160
+ });
161
+ }
162
+ else if (options.writeOutput) {
163
+ await saveSetupOutput(outputValue, options.customPath);
164
+ }
165
+ printInitResult(outputValue, options, installResult);
166
+ }
167
+ async function runNonInteractiveInputSetup(options, flow) {
168
+ const lines = await readPipedLines();
169
+ const state = { index: 0 };
170
+ const provider = promptChoiceFromLines("Which provider do you want to configure?", buildProviderChoices(), lines, state);
171
+ const answers = {};
172
+ for (const question of buildQuickSetupFlow(flow)) {
173
+ if (!question.choices) {
174
+ throw new Error(`Question ${question.id} requires choices in non-interactive mode.`);
175
+ }
176
+ const value = promptChoiceFromLines(question.prompt, question.choices, lines, state);
177
+ answers[question.id] = value;
178
+ }
179
+ const outputValue = createPersistedSetupOutput(answers, provider, flow);
180
+ await persistInitResult(outputValue, options);
181
+ }
182
+ async function runInteractiveSetup(options, flow) {
183
+ if (!input.isTTY) {
184
+ await runNonInteractiveInputSetup(options, flow);
185
+ return;
186
+ }
187
+ const rl = createInterface({ input, output });
188
+ try {
189
+ const provider = await promptChoice(rl, "Which provider do you want to configure?", buildProviderChoices());
190
+ const answers = {};
191
+ for (const question of buildQuickSetupFlow(flow)) {
192
+ if (question.kind !== "single_choice" || !question.choices) {
193
+ const response = await rl.question(`${question.prompt}\n> `);
194
+ if (response.trim().length === 0) {
195
+ console.log("This field cannot be empty.");
196
+ return await runInteractiveSetup(options, flow);
197
+ }
198
+ answers[question.id] = response.trim();
199
+ continue;
200
+ }
201
+ const value = await promptChoice(rl, question.prompt, question.choices);
202
+ answers[question.id] = value;
203
+ }
204
+ const outputValue = createPersistedSetupOutput(answers, provider, flow);
205
+ await persistInitResult(outputValue, options);
206
+ }
207
+ finally {
208
+ rl.close();
209
+ }
210
+ }
211
+ async function printStoredSetup(customPath, jsonOutput) {
212
+ const setup = await loadSetupOutput(customPath);
213
+ console.log(jsonOutput ? serializeSetupOutput(setup) : renderSetupSummary(setup));
214
+ }
215
+ async function installStoredSetup(customPath, runtimePath, jsonOutput) {
216
+ const result = await installRuntimeConfigFromStoredSetup({
217
+ setupPath: customPath,
218
+ runtimePath
219
+ });
220
+ if (jsonOutput) {
221
+ console.log(JSON.stringify(result, null, 2));
222
+ return;
223
+ }
224
+ console.log(renderInstallSummary(result));
225
+ }
226
+ async function main() {
227
+ const args = parseArgs(process.argv.slice(2));
228
+ if (!args._command || args._command === "help" || args._command === "--help") {
229
+ printUsage();
230
+ process.exit(0);
231
+ }
232
+ if (args._command === "show") {
233
+ await printStoredSetup(typeof args.path === "string" ? args.path : undefined, args.json === true);
234
+ return;
235
+ }
236
+ if (args._command === "install") {
237
+ await installStoredSetup(typeof args.path === "string" ? args.path : undefined, typeof args["runtime-path"] === "string" ? args["runtime-path"] : undefined, args.json === true);
238
+ return;
239
+ }
240
+ if (args._command !== "init") {
241
+ printUsage();
242
+ throw new Error(`Unknown command: ${String(args._command)}`);
243
+ }
244
+ if (hasCompleteFlagInput(args)) {
245
+ const provider = requireString(args, "provider");
246
+ const outputValue = createPersistedSetupOutput(toSetupAnswers(args), provider, resolveSetupFlow(args));
247
+ const shouldWrite = args.write === true;
248
+ const shouldInstall = args.install === true;
249
+ const customPath = typeof args.path === "string" ? args.path : undefined;
250
+ const runtimePath = typeof args["runtime-path"] === "string"
251
+ ? args["runtime-path"]
252
+ : undefined;
253
+ await persistInitResult(outputValue, {
254
+ jsonOutput: args.json === true,
255
+ writeOutput: shouldWrite || shouldInstall,
256
+ customPath,
257
+ installRuntime: shouldInstall,
258
+ runtimePath
259
+ });
260
+ return;
261
+ }
262
+ await runInteractiveSetup({
263
+ jsonOutput: args.json === true,
264
+ writeOutput: args.write === true || args.install === true,
265
+ customPath: typeof args.path === "string" ? args.path : undefined,
266
+ installRuntime: args.install === true,
267
+ runtimePath: typeof args["runtime-path"] === "string"
268
+ ? args["runtime-path"]
269
+ : undefined
270
+ }, resolveSetupFlow(args));
271
+ }
272
+ main().catch((error) => {
273
+ console.error(error instanceof Error ? error.message : String(error));
274
+ process.exit(1);
275
+ });
@@ -0,0 +1,4 @@
1
+ export * from "./numbered-checkpoint.js";
2
+ export * from "./onboarding.js";
3
+ export * from "./persistence.js";
4
+ export * from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./numbered-checkpoint.js";
2
+ export * from "./onboarding.js";
3
+ export * from "./persistence.js";
4
+ export * from "./types.js";
@@ -0,0 +1,3 @@
1
+ import type { NumberedCheckpointSpec, ParsedCheckpointSelection } from "./types.js";
2
+ export declare function buildNumberedCheckpointPrompt(spec: NumberedCheckpointSpec): string;
3
+ export declare function parseNumberedCheckpointResponse(spec: NumberedCheckpointSpec, input: string): ParsedCheckpointSelection | null;
@@ -0,0 +1,42 @@
1
+ export function buildNumberedCheckpointPrompt(spec) {
2
+ const lines = [`CHECKPOINT: ${spec.title}`, ""];
3
+ if (spec.instructions) {
4
+ lines.push(spec.instructions, "");
5
+ }
6
+ spec.options.forEach((option, index) => {
7
+ lines.push(`${index + 1}. ${option.label}`);
8
+ });
9
+ lines.push("");
10
+ if (spec.allowRationale) {
11
+ lines.push("Reply with one number only, or one number followed by a short rationale on the next line.");
12
+ }
13
+ else {
14
+ lines.push(`Reply with one number only: ${spec.options.map((_, index) => index + 1).join(", ")}.`);
15
+ }
16
+ return lines.join("\n");
17
+ }
18
+ export function parseNumberedCheckpointResponse(spec, input) {
19
+ const trimmed = input.trim();
20
+ if (trimmed.length === 0) {
21
+ return null;
22
+ }
23
+ const [firstLine, ...restLines] = trimmed.split(/\r?\n/);
24
+ if (!/^\d+$/.test(firstLine.trim())) {
25
+ return null;
26
+ }
27
+ const index = Number(firstLine.trim()) - 1;
28
+ const option = spec.options[index];
29
+ if (!option) {
30
+ return null;
31
+ }
32
+ const rationale = restLines.join("\n").trim();
33
+ if (!spec.allowRationale && rationale.length > 0) {
34
+ return null;
35
+ }
36
+ return {
37
+ index,
38
+ value: option.value,
39
+ label: option.label,
40
+ rationale: rationale.length > 0 ? rationale : undefined
41
+ };
42
+ }
@@ -0,0 +1,11 @@
1
+ import type { ProviderKind, SetupChoice, SetupFlow, ProviderSelection, ResearcherProfileSeed, SetupAnswers, SetupPersistedOutput, SetupQuestion } from "./types.js";
2
+ export declare function buildQuickSetupFlow(flow?: SetupFlow): SetupQuestion[];
3
+ export declare function buildProviderChoices(): SetupChoice[];
4
+ export declare function buildFieldChoices(): SetupChoice[];
5
+ export declare function buildCareerStageChoices(): SetupChoice[];
6
+ export declare function buildProjectTypeChoices(): SetupChoice[];
7
+ export declare function isFallbackChoice(choice: SetupChoice | undefined): boolean;
8
+ export declare function normalizeProviderChoice(choice: string): ProviderKind;
9
+ export declare function createResearcherProfileSeed(answers: SetupAnswers): ResearcherProfileSeed;
10
+ export declare function resolveProviderSelection(provider: ProviderSelection["provider"]): ProviderSelection;
11
+ export declare function createPersistedSetupOutput(answers: SetupAnswers, provider: ProviderSelection["provider"], flow?: SetupFlow): SetupPersistedOutput;
@@ -0,0 +1,212 @@
1
+ import { createEmptyResearchState } from "@longtable/memory";
2
+ export function buildQuickSetupFlow(flow = "quickstart") {
3
+ const baseQuestions = [
4
+ {
5
+ id: "field",
6
+ prompt: "Before we begin, which research field best matches your work right now?",
7
+ required: true,
8
+ kind: "single_choice",
9
+ choices: buildFieldChoices()
10
+ },
11
+ {
12
+ id: "careerStage",
13
+ prompt: "What kind of researcher role best fits you today?",
14
+ required: true,
15
+ kind: "single_choice",
16
+ choices: buildCareerStageChoices()
17
+ },
18
+ {
19
+ id: "experienceLevel",
20
+ prompt: "How comfortable are you making independent research design decisions right now?",
21
+ required: true,
22
+ kind: "single_choice",
23
+ choices: [
24
+ { id: "novice", label: "Novice", description: "Needs stronger checkpoint guidance." },
25
+ { id: "intermediate", label: "Intermediate", description: "Wants balanced guidance." },
26
+ { id: "advanced", label: "Advanced", description: "Prefers lighter intervention." }
27
+ ]
28
+ },
29
+ {
30
+ id: "preferredCheckpointIntensity",
31
+ prompt: "How strongly should LongTable challenge or slow you down by default?",
32
+ required: true,
33
+ kind: "single_choice",
34
+ choices: [
35
+ { id: "low", label: "Low", description: "Mostly lightweight logging and advisory prompts." },
36
+ { id: "balanced", label: "Balanced", description: "Recommended by default." },
37
+ { id: "high", label: "High", description: "Frequent structured commitment prompts." }
38
+ ]
39
+ },
40
+ {
41
+ id: "humanAuthorshipSignal",
42
+ prompt: "When a piece of writing still feels like yours, what usually makes that true?",
43
+ required: false,
44
+ kind: "single_choice",
45
+ choices: [
46
+ { id: "personal experience in the narrative", label: "Personal experience", description: "Humanity appears through lived experience and situated voice." },
47
+ { id: "visible judgment and reasoning path", label: "Judgment trail", description: "Humanity appears through explicit choices and reasoning history." },
48
+ { id: "contextual nuance that avoids generic fluency", label: "Contextual nuance", description: "Humanity appears through specificity and non-generic framing." },
49
+ { id: "a distinctive voice that still sounds like the researcher", label: "Distinctive voice", description: "Humanity appears through authorial tone and cadence." },
50
+ { id: "other", label: "None of the above", description: "Enter a custom authorship signal.", fallbackToText: true }
51
+ ]
52
+ }
53
+ ];
54
+ if (flow === "quickstart") {
55
+ return baseQuestions;
56
+ }
57
+ return [
58
+ ...baseQuestions,
59
+ {
60
+ id: "preferredEntryMode",
61
+ prompt: "When you first open LongTable, where do you want it to begin?",
62
+ required: false,
63
+ kind: "single_choice",
64
+ choices: [
65
+ { id: "explore", label: "Explore", description: "Open the problem, surface tensions, and ask better questions." },
66
+ { id: "review", label: "Review", description: "Critically inspect a claim, plan, or draft." },
67
+ { id: "critique", label: "Critique", description: "Stress-test weak assumptions and generate counterarguments." },
68
+ { id: "draft", label: "Draft", description: "Write while preserving narrative trace and authorship." },
69
+ { id: "commit", label: "Commit", description: "Slow down and make an explicit research decision." }
70
+ ]
71
+ },
72
+ {
73
+ id: "weakestDomain",
74
+ prompt: "Where do you most want LongTable to push back on you first?",
75
+ required: false,
76
+ kind: "single_choice",
77
+ choices: [
78
+ { id: "theory", label: "Theory", description: "Sharpen framing, constructs, and theoretical defensibility." },
79
+ { id: "methodology", label: "Methodology", description: "Question design fit, sampling, and study structure." },
80
+ { id: "measurement", label: "Measurement", description: "Challenge scales, validity, and operationalization." },
81
+ { id: "analysis", label: "Analysis", description: "Probe analytic logic, interpretation, and evidence claims." },
82
+ { id: "writing", label: "Writing", description: "Preserve voice while improving clarity and argument flow." }
83
+ ]
84
+ },
85
+ {
86
+ id: "panelPreference",
87
+ prompt: "How visible should disagreement between roles be by default?",
88
+ required: false,
89
+ kind: "single_choice",
90
+ choices: [
91
+ { id: "synthesis_only", label: "Synthesis only", description: "Show one LongTable answer unless I ask for more." },
92
+ { id: "show_on_conflict", label: "Show on conflict", description: "Surface panel disagreement when roles materially diverge." },
93
+ { id: "always_visible", label: "Always visible", description: "Keep panel opinions visible by default." }
94
+ ]
95
+ }
96
+ ];
97
+ }
98
+ export function buildProviderChoices() {
99
+ return [
100
+ {
101
+ id: "codex",
102
+ label: "Codex",
103
+ description: "Use numbered checkpoints and Codex runtime surfaces."
104
+ },
105
+ {
106
+ id: "claude",
107
+ label: "Claude",
108
+ description: "Use structured-question capable Claude runtime surfaces."
109
+ }
110
+ ];
111
+ }
112
+ export function buildFieldChoices() {
113
+ return [
114
+ { id: "education", label: "Education", description: "Learning, instruction, or educational technology." },
115
+ { id: "psychology", label: "Psychology", description: "Behavior, cognition, or social psychology." },
116
+ { id: "hrd", label: "HRD", description: "Human resource development and workplace learning." },
117
+ { id: "management", label: "Management", description: "Organizations, strategy, or business research." },
118
+ { id: "other", label: "None of the above", description: "Enter a custom field.", fallbackToText: true }
119
+ ];
120
+ }
121
+ export function buildCareerStageChoices() {
122
+ return [
123
+ { id: "doctoral", label: "Doctoral student", description: "Currently in doctoral training." },
124
+ { id: "postdoctoral", label: "Postdoctoral researcher", description: "Research-focused early career stage." },
125
+ { id: "faculty", label: "Faculty", description: "Professor or lecturer role." },
126
+ { id: "industry", label: "Industry researcher", description: "Research outside a university role." },
127
+ { id: "other", label: "None of the above", description: "Enter a custom career stage.", fallbackToText: true }
128
+ ];
129
+ }
130
+ export function buildProjectTypeChoices() {
131
+ return [
132
+ { id: "journal article", label: "Journal article", description: "A paper targeted at an academic journal." },
133
+ { id: "conference paper", label: "Conference paper", description: "A paper targeted at a conference venue." },
134
+ { id: "proposal", label: "Proposal", description: "A grant, dissertation, or research proposal." },
135
+ { id: "mixed-methods study", label: "Mixed-methods study", description: "A study design spanning multiple methods." },
136
+ { id: "other", label: "None of the above", description: "Enter a custom project type.", fallbackToText: true }
137
+ ];
138
+ }
139
+ export function isFallbackChoice(choice) {
140
+ return choice?.fallbackToText === true;
141
+ }
142
+ export function normalizeProviderChoice(choice) {
143
+ return choice === "claude" ? "claude" : "codex";
144
+ }
145
+ export function createResearcherProfileSeed(answers) {
146
+ return {
147
+ currentProjectType: answers.currentProjectType ?? "unspecified research task",
148
+ ...answers,
149
+ aiAutonomyPreference: answers.experienceLevel === "advanced"
150
+ ? "balanced"
151
+ : answers.experienceLevel === "intermediate"
152
+ ? "balanced"
153
+ : "low"
154
+ };
155
+ }
156
+ export function resolveProviderSelection(provider) {
157
+ if (provider === "claude") {
158
+ return {
159
+ provider,
160
+ checkpointProtocol: "native_structured",
161
+ supportsStructuredQuestions: true
162
+ };
163
+ }
164
+ return {
165
+ provider,
166
+ checkpointProtocol: "numbered",
167
+ supportsStructuredQuestions: false
168
+ };
169
+ }
170
+ export function createPersistedSetupOutput(answers, provider, flow = "quickstart") {
171
+ const profileSeed = createResearcherProfileSeed(answers);
172
+ const initialState = createEmptyResearchState();
173
+ initialState.explicitState = {
174
+ field: profileSeed.field,
175
+ careerStage: profileSeed.careerStage,
176
+ experienceLevel: profileSeed.experienceLevel,
177
+ ...(profileSeed.currentProjectType
178
+ ? { currentProjectType: profileSeed.currentProjectType }
179
+ : {}),
180
+ preferredCheckpointIntensity: profileSeed.preferredCheckpointIntensity,
181
+ ...(profileSeed.humanAuthorshipSignal
182
+ ? { humanAuthorshipSignal: profileSeed.humanAuthorshipSignal }
183
+ : {}),
184
+ ...(profileSeed.preferredEntryMode
185
+ ? { preferredEntryMode: profileSeed.preferredEntryMode }
186
+ : {}),
187
+ ...(profileSeed.weakestDomain
188
+ ? { weakestDomain: profileSeed.weakestDomain }
189
+ : {}),
190
+ ...(profileSeed.panelPreference
191
+ ? { panelPreference: profileSeed.panelPreference }
192
+ : {})
193
+ };
194
+ if (profileSeed.humanAuthorshipSignal) {
195
+ initialState.narrativeTraces.push({
196
+ id: "setup-human-authorship-signal",
197
+ timestamp: new Date().toISOString(),
198
+ source: "setup",
199
+ traceType: "voice",
200
+ summary: `Researcher identifies human authorship through ${profileSeed.humanAuthorshipSignal}.`,
201
+ visibility: "explicit",
202
+ importance: "high"
203
+ });
204
+ }
205
+ return {
206
+ setupFlow: flow,
207
+ profileSeed,
208
+ providerSelection: resolveProviderSelection(provider),
209
+ defaultInteractionMode: profileSeed.preferredEntryMode ?? "explore",
210
+ initialState
211
+ };
212
+ }
@@ -0,0 +1,21 @@
1
+ import type { ProviderKind } from "@longtable/core";
2
+ import type { RuntimeConfigTarget, SetupAnswers, SetupInstallResult, SetupPersistedOutput, SetupStorageTarget } from "./types.js";
3
+ export declare function serializeSetupOutput(output: SetupPersistedOutput): string;
4
+ export declare function parseSetupOutput(input: string): SetupPersistedOutput;
5
+ export declare function createSetupOutputExample(provider: ProviderKind, overrides?: Partial<SetupAnswers>): SetupPersistedOutput;
6
+ export declare function renderSetupSummary(output: SetupPersistedOutput): string;
7
+ export declare function resolveDefaultSetupPath(customPath?: string): SetupStorageTarget;
8
+ export declare function resolveDefaultRuntimeConfigPath(provider: ProviderKind, customPath?: string): RuntimeConfigTarget;
9
+ export declare function saveSetupOutput(output: SetupPersistedOutput, customPath?: string): Promise<SetupStorageTarget>;
10
+ export declare function loadSetupOutput(customPath?: string): Promise<SetupPersistedOutput>;
11
+ export declare function renderRuntimeConfig(output: SetupPersistedOutput, setupPath: string): string;
12
+ export declare function writeRuntimeConfig(output: SetupPersistedOutput, setupPath: string, customPath?: string): Promise<RuntimeConfigTarget>;
13
+ export declare function saveSetupAndRuntimeConfig(output: SetupPersistedOutput, options?: {
14
+ setupPath?: string;
15
+ runtimePath?: string;
16
+ }): Promise<SetupInstallResult>;
17
+ export declare function installRuntimeConfigFromStoredSetup(options?: {
18
+ setupPath?: string;
19
+ runtimePath?: string;
20
+ }): Promise<SetupInstallResult>;
21
+ export declare function renderInstallSummary(result: SetupInstallResult): string;
@@ -0,0 +1,178 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { createPersistedSetupOutput } from "./onboarding.js";
5
+ export function serializeSetupOutput(output) {
6
+ return JSON.stringify(output, null, 2);
7
+ }
8
+ export function parseSetupOutput(input) {
9
+ return JSON.parse(input);
10
+ }
11
+ export function createSetupOutputExample(provider, overrides = {}) {
12
+ return createPersistedSetupOutput({
13
+ field: overrides.field ?? "education",
14
+ careerStage: overrides.careerStage ?? "doctoral",
15
+ experienceLevel: overrides.experienceLevel ?? "intermediate",
16
+ preferredCheckpointIntensity: overrides.preferredCheckpointIntensity ?? "balanced"
17
+ }, provider);
18
+ }
19
+ export function renderSetupSummary(output) {
20
+ const lines = [
21
+ "LongTable setup summary",
22
+ `setup flow: ${output.setupFlow}`,
23
+ `provider: ${output.providerSelection.provider}`,
24
+ `checkpoint protocol: ${output.providerSelection.checkpointProtocol}`,
25
+ `default mode: ${output.defaultInteractionMode}`,
26
+ `field: ${output.profileSeed.field}`,
27
+ `career stage: ${output.profileSeed.careerStage}`,
28
+ `experience: ${output.profileSeed.experienceLevel}`,
29
+ `checkpoint intensity: ${output.profileSeed.preferredCheckpointIntensity}`
30
+ ];
31
+ if (output.profileSeed.weakestDomain) {
32
+ lines.push(`challenge first: ${output.profileSeed.weakestDomain}`);
33
+ }
34
+ if (output.profileSeed.panelPreference) {
35
+ lines.push(`panel preference: ${output.profileSeed.panelPreference}`);
36
+ }
37
+ return lines.join("\n");
38
+ }
39
+ export function resolveDefaultSetupPath(customPath) {
40
+ const targetPath = customPath
41
+ ? resolve(customPath)
42
+ : join(homedir(), ".longtable", "setup.json");
43
+ return {
44
+ path: targetPath,
45
+ directory: dirname(targetPath)
46
+ };
47
+ }
48
+ export function resolveDefaultRuntimeConfigPath(provider, customPath) {
49
+ const format = provider === "codex" ? "toml" : "json";
50
+ const fileName = provider === "codex" ? "longtable.toml" : "longtable.json";
51
+ const targetPath = customPath
52
+ ? resolve(customPath)
53
+ : join(homedir(), ".longtable", "runtime", provider, fileName);
54
+ return {
55
+ provider,
56
+ path: targetPath,
57
+ directory: dirname(targetPath),
58
+ format
59
+ };
60
+ }
61
+ export async function saveSetupOutput(output, customPath) {
62
+ const target = resolveDefaultSetupPath(customPath);
63
+ await mkdir(target.directory, { recursive: true });
64
+ await writeFile(target.path, serializeSetupOutput(output), "utf8");
65
+ return target;
66
+ }
67
+ export async function loadSetupOutput(customPath) {
68
+ const target = resolveDefaultSetupPath(customPath);
69
+ const content = await readFile(target.path, "utf8");
70
+ return parseSetupOutput(content);
71
+ }
72
+ function resolveRuntimeGuidance(output) {
73
+ if (output.providerSelection.provider === "codex") {
74
+ return {
75
+ askAtLeastTwoQuestionsInExplore: true,
76
+ preserveNarrativeTraceInDraft: true,
77
+ requireWhyMayBeWrongInReview: true,
78
+ questionBiasCompensation: "strong"
79
+ };
80
+ }
81
+ return {
82
+ askAtLeastTwoQuestionsInExplore: true,
83
+ preserveNarrativeTraceInDraft: true,
84
+ requireWhyMayBeWrongInReview: true,
85
+ structuredQuestionBias: "strong"
86
+ };
87
+ }
88
+ function renderCodexRuntimeConfig(output, setupPath) {
89
+ const runtimeGuidance = resolveRuntimeGuidance(output);
90
+ return [
91
+ "[longtable]",
92
+ `provider = "${output.providerSelection.provider}"`,
93
+ `setup_path = "${setupPath}"`,
94
+ `checkpoint_protocol = "${output.providerSelection.checkpointProtocol}"`,
95
+ `default_interaction_mode = "${output.defaultInteractionMode}"`,
96
+ `setup_flow = "${output.setupFlow}"`,
97
+ "",
98
+ "[longtable.profile]",
99
+ `field = "${output.profileSeed.field}"`,
100
+ `career_stage = "${output.profileSeed.careerStage}"`,
101
+ `experience_level = "${output.profileSeed.experienceLevel}"`,
102
+ ...(output.profileSeed.currentProjectType && output.profileSeed.currentProjectType !== "unspecified research task"
103
+ ? [`current_project_type = "${output.profileSeed.currentProjectType}"`]
104
+ : []),
105
+ ...(output.profileSeed.weakestDomain
106
+ ? [`weakest_domain = "${output.profileSeed.weakestDomain}"`]
107
+ : []),
108
+ ...(output.profileSeed.panelPreference
109
+ ? [`panel_preference = "${output.profileSeed.panelPreference}"`]
110
+ : []),
111
+ "",
112
+ "[longtable.runtime_guidance]",
113
+ `ask_at_least_two_questions_in_explore = ${runtimeGuidance.askAtLeastTwoQuestionsInExplore}`,
114
+ `preserve_narrative_trace_in_draft = ${runtimeGuidance.preserveNarrativeTraceInDraft}`,
115
+ `require_why_may_be_wrong_in_review = ${runtimeGuidance.requireWhyMayBeWrongInReview}`,
116
+ `question_bias_compensation = "${runtimeGuidance.questionBiasCompensation}"`
117
+ ].join("\n");
118
+ }
119
+ function renderClaudeRuntimeConfig(output, setupPath) {
120
+ const runtimeGuidance = resolveRuntimeGuidance(output);
121
+ return JSON.stringify({
122
+ setupPath,
123
+ provider: output.providerSelection.provider,
124
+ checkpointProtocol: output.providerSelection.checkpointProtocol,
125
+ defaultInteractionMode: output.defaultInteractionMode,
126
+ setupFlow: output.setupFlow,
127
+ profileSummary: {
128
+ field: output.profileSeed.field,
129
+ careerStage: output.profileSeed.careerStage,
130
+ experienceLevel: output.profileSeed.experienceLevel,
131
+ currentProjectType: output.profileSeed.currentProjectType && output.profileSeed.currentProjectType !== "unspecified research task"
132
+ ? output.profileSeed.currentProjectType
133
+ : undefined,
134
+ weakestDomain: output.profileSeed.weakestDomain,
135
+ panelPreference: output.profileSeed.panelPreference
136
+ },
137
+ runtimeGuidance
138
+ }, null, 2);
139
+ }
140
+ export function renderRuntimeConfig(output, setupPath) {
141
+ return output.providerSelection.provider === "codex"
142
+ ? renderCodexRuntimeConfig(output, setupPath)
143
+ : renderClaudeRuntimeConfig(output, setupPath);
144
+ }
145
+ export async function writeRuntimeConfig(output, setupPath, customPath) {
146
+ const target = resolveDefaultRuntimeConfigPath(output.providerSelection.provider, customPath);
147
+ await mkdir(target.directory, { recursive: true });
148
+ await writeFile(target.path, renderRuntimeConfig(output, setupPath), "utf8");
149
+ return target;
150
+ }
151
+ export async function saveSetupAndRuntimeConfig(output, options = {}) {
152
+ const setupTarget = await saveSetupOutput(output, options.setupPath);
153
+ const runtimeTarget = await writeRuntimeConfig(output, setupTarget.path, options.runtimePath);
154
+ return {
155
+ provider: output.providerSelection.provider,
156
+ setupTarget,
157
+ runtimeTarget
158
+ };
159
+ }
160
+ export async function installRuntimeConfigFromStoredSetup(options = {}) {
161
+ const setupTarget = resolveDefaultSetupPath(options.setupPath);
162
+ const output = await loadSetupOutput(options.setupPath);
163
+ const runtimeTarget = await writeRuntimeConfig(output, setupTarget.path, options.runtimePath);
164
+ return {
165
+ provider: output.providerSelection.provider,
166
+ setupTarget,
167
+ runtimeTarget
168
+ };
169
+ }
170
+ export function renderInstallSummary(result) {
171
+ return [
172
+ "LongTable runtime install summary",
173
+ `provider: ${result.provider}`,
174
+ `setup path: ${result.setupTarget.path}`,
175
+ `runtime config path: ${result.runtimeTarget.path}`,
176
+ `runtime config format: ${result.runtimeTarget.format}`
177
+ ].join("\n");
178
+ }
@@ -0,0 +1,73 @@
1
+ import type { CheckpointIntensity, ExperienceLevel, InteractionMode, ProviderKind, ResearcherConfidenceByDomain, ResearchState } from "@longtable/core";
2
+ export interface SetupChoice {
3
+ id: string;
4
+ label: string;
5
+ description: string;
6
+ fallbackToText?: boolean;
7
+ }
8
+ export interface SetupQuestion {
9
+ id: string;
10
+ prompt: string;
11
+ required: boolean;
12
+ kind: "single_choice" | "text";
13
+ choices?: SetupChoice[];
14
+ }
15
+ export type SetupFlow = "quickstart" | "interview";
16
+ export interface SetupAnswers {
17
+ field: string;
18
+ careerStage: string;
19
+ experienceLevel: ExperienceLevel;
20
+ currentProjectType?: string;
21
+ preferredCheckpointIntensity: CheckpointIntensity;
22
+ humanAuthorshipSignal?: string;
23
+ preferredEntryMode?: Exclude<InteractionMode, "submit">;
24
+ weakestDomain?: Extract<keyof ResearcherConfidenceByDomain, string>;
25
+ panelPreference?: "synthesis_only" | "show_on_conflict" | "always_visible";
26
+ }
27
+ export interface ResearcherProfileSeed extends SetupAnswers {
28
+ aiAutonomyPreference: "low" | "balanced" | "high";
29
+ }
30
+ export interface ProviderSelection {
31
+ provider: ProviderKind;
32
+ checkpointProtocol: "native_structured" | "numbered";
33
+ supportsStructuredQuestions: boolean;
34
+ }
35
+ export interface SetupPersistedOutput {
36
+ setupFlow: SetupFlow;
37
+ profileSeed: ResearcherProfileSeed;
38
+ providerSelection: ProviderSelection;
39
+ defaultInteractionMode: Exclude<InteractionMode, "submit">;
40
+ initialState: ResearchState;
41
+ }
42
+ export interface SetupStorageTarget {
43
+ path: string;
44
+ directory: string;
45
+ }
46
+ export interface RuntimeConfigTarget {
47
+ provider: ProviderKind;
48
+ path: string;
49
+ directory: string;
50
+ format: "toml" | "json";
51
+ }
52
+ export interface SetupInstallResult {
53
+ provider: ProviderKind;
54
+ setupTarget: SetupStorageTarget;
55
+ runtimeTarget: RuntimeConfigTarget;
56
+ }
57
+ export interface NumberedCheckpointOption {
58
+ value: string;
59
+ label: string;
60
+ }
61
+ export interface NumberedCheckpointSpec {
62
+ title: string;
63
+ instructions?: string;
64
+ options: NumberedCheckpointOption[];
65
+ allowRationale?: boolean;
66
+ }
67
+ export interface ParsedCheckpointSelection {
68
+ index: number;
69
+ value: string;
70
+ label: string;
71
+ rationale?: string;
72
+ }
73
+ export type { CheckpointIntensity, ExperienceLevel, ProviderKind, ResearchState } from "@longtable/core";
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ {
2
+ "profileSeed": {
3
+ "field": "psychology",
4
+ "careerStage": "faculty",
5
+ "experienceLevel": "advanced",
6
+ "preferredCheckpointIntensity": "low",
7
+ "aiAutonomyPreference": "balanced"
8
+ },
9
+ "providerSelection": {
10
+ "provider": "claude",
11
+ "checkpointProtocol": "native_structured",
12
+ "supportsStructuredQuestions": true
13
+ },
14
+ "defaultInteractionMode": "explore",
15
+ "initialState": {
16
+ "explicitState": {
17
+ "field": "psychology",
18
+ "careerStage": "faculty",
19
+ "experienceLevel": "advanced",
20
+ "preferredCheckpointIntensity": "low"
21
+ },
22
+ "inferredHypotheses": [],
23
+ "openTensions": [],
24
+ "decisionLog": [],
25
+ "artifactRecords": []
26
+ }
27
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "profileSeed": {
3
+ "field": "education",
4
+ "careerStage": "doctoral",
5
+ "experienceLevel": "intermediate",
6
+ "preferredCheckpointIntensity": "balanced",
7
+ "aiAutonomyPreference": "balanced"
8
+ },
9
+ "providerSelection": {
10
+ "provider": "codex",
11
+ "checkpointProtocol": "numbered",
12
+ "supportsStructuredQuestions": false
13
+ },
14
+ "defaultInteractionMode": "explore",
15
+ "initialState": {
16
+ "explicitState": {
17
+ "field": "education",
18
+ "careerStage": "doctoral",
19
+ "experienceLevel": "intermediate",
20
+ "preferredCheckpointIntensity": "balanced"
21
+ },
22
+ "inferredHypotheses": [],
23
+ "openTensions": [],
24
+ "decisionLog": [],
25
+ "artifactRecords": []
26
+ }
27
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@longtable/setup",
3
+ "version": "0.1.9",
4
+ "private": false,
5
+ "description": "Researcher onboarding and setup flows for LongTable",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "CHANGELOG.md",
19
+ "examples"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -p tsconfig.json",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "@longtable/core": "0.1.9",
27
+ "@longtable/memory": "0.1.9"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.0.0",
31
+ "typescript": "^5.6.0"
32
+ },
33
+ "keywords": [
34
+ "longtable",
35
+ "research",
36
+ "setup",
37
+ "onboarding",
38
+ "codex",
39
+ "claude"
40
+ ],
41
+ "author": "Hosung You",
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/HosungYou/LongTable.git"
46
+ },
47
+ "homepage": "https://github.com/HosungYou/LongTable#readme",
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ }
54
+ }