intentdocs 0.10.1 → 0.12.0

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
@@ -182,7 +182,7 @@ The intentdocs MCP server exposes **tools** (actions your agent can take) and **
182
182
 
183
183
  Give Claude Code or Cursor native tool and resource access to your story map.
184
184
 
185
- Add to `~/.claude/settings.json` (Claude Code) or your Cursor MCP config:
185
+ `intent setup` registers it for Claude Code in the project's `.mcp.json`. To add it yourself, run `claude mcp add --scope project intent -- npx intentdocs mcp` in the project, or add this to `.mcp.json` (Claude Code) or your Cursor MCP config:
186
186
 
187
187
  ```json
188
188
  {
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-KPQ42YE6.js";
5
5
  import {
6
6
  requireConfig
7
- } from "./chunk-JX2RLGOM.js";
7
+ } from "./chunk-RGFGTIIH.js";
8
8
  import {
9
9
  closePrompt,
10
10
  confirm,
@@ -0,0 +1,171 @@
1
+ import {
2
+ bindingLabel,
3
+ clearAuth,
4
+ getAuth,
5
+ saveAuth,
6
+ validateAuth
7
+ } from "./chunk-XZE2XP6O.js";
8
+ import {
9
+ API_BASE
10
+ } from "./chunk-UYTVTYNB.js";
11
+
12
+ // src/commands/login.ts
13
+ import { exec } from "child_process";
14
+ function finishLogin(data) {
15
+ saveAuth({
16
+ token: data.token,
17
+ apiBase: API_BASE,
18
+ ...data.userId && { userId: data.userId },
19
+ binding: data.binding ?? null,
20
+ ...data.posthogKey && { posthogKey: data.posthogKey },
21
+ ...data.posthogHost && { posthogHost: data.posthogHost }
22
+ });
23
+ console.log(`
24
+
25
+ \u2705 Logged in as ${data.userId ?? "unknown user"} \u2014 token ${bindingLabel(data.binding)}
26
+ `);
27
+ if (data.binding?.approvalStatus === "pending") {
28
+ console.log("An admin has been asked to approve it. Until they do, this token reads your personal projects only.\n");
29
+ }
30
+ console.log("You can now run: npx intentdocs init");
31
+ console.log(
32
+ "Optional: add your Anthropic API key to unlock AI features on any plan \u2014 npx intentdocs keys set"
33
+ );
34
+ }
35
+ function openBrowser(url) {
36
+ const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
37
+ exec(cmd);
38
+ }
39
+ function sleep(ms) {
40
+ return new Promise((r) => setTimeout(r, ms));
41
+ }
42
+ async function login() {
43
+ const existing = getAuth();
44
+ if (existing) {
45
+ const result = await validateAuth();
46
+ if (result.valid) {
47
+ console.log(`Already logged in (user: ${result.userId}). Run \`intentdocs logout\` to switch accounts.`);
48
+ return;
49
+ }
50
+ switch (result.reason) {
51
+ case "revoked":
52
+ console.log("Your stored token has been revoked. Starting a fresh login...\n");
53
+ break;
54
+ case "not_found":
55
+ console.log("Your stored token is no longer valid. Starting a fresh login...\n");
56
+ break;
57
+ case "network_error":
58
+ console.error("Could not reach intentdocs.com to verify your token. Check your internet connection and try again.");
59
+ process.exit(1);
60
+ break;
61
+ }
62
+ clearAuth();
63
+ }
64
+ console.log("Logging in to intentdocs...\n");
65
+ const res = await fetch(`${API_BASE}/api/cli/auth`, { method: "POST" });
66
+ if (!res.ok) {
67
+ console.error("Failed to start login flow. Try again.");
68
+ process.exit(1);
69
+ }
70
+ const { code } = await res.json();
71
+ const authUrl = `${API_BASE}/cli/auth?code=${code}`;
72
+ console.log("Opening browser to authorize...");
73
+ console.log(`If it doesn't open, visit: ${authUrl}
74
+ `);
75
+ openBrowser(authUrl);
76
+ const POLL_INTERVAL_MS = 3e3;
77
+ const INITIAL_TIMEOUT_S = 300;
78
+ const EXTEND_TIMEOUT_S = 120;
79
+ let totalWaitedS = 0;
80
+ let phase = "initial";
81
+ let timeoutS = INITIAL_TIMEOUT_S;
82
+ while (totalWaitedS < timeoutS) {
83
+ const elapsed = totalWaitedS;
84
+ const minutesLeft = Math.ceil((timeoutS - elapsed) / 60);
85
+ if (elapsed === 0) {
86
+ process.stdout.write("Waiting for browser approval...");
87
+ } else if (elapsed % 30 === 0) {
88
+ process.stdout.write(`
89
+ Still waiting for approval (${minutesLeft}m remaining)...`);
90
+ } else {
91
+ process.stdout.write(".");
92
+ }
93
+ await sleep(POLL_INTERVAL_MS);
94
+ totalWaitedS += POLL_INTERVAL_MS / 1e3;
95
+ try {
96
+ const checkRes = await fetch(`${API_BASE}/api/cli/auth?code=${code}`);
97
+ if (!checkRes.ok) continue;
98
+ const data = await checkRes.json();
99
+ if (data.status === "approved" && data.token) {
100
+ finishLogin(data);
101
+ return;
102
+ }
103
+ if (data.status === "expired") {
104
+ console.error("\n\nThe server-side login request expired. Run `npx intentdocs login` to start a new one.");
105
+ process.exit(1);
106
+ }
107
+ } catch {
108
+ }
109
+ }
110
+ if (phase === "initial") {
111
+ console.log("\n\nThe initial wait has ended. The browser approval hasn't come through yet.");
112
+ console.log("This can happen if you needed to create an account or got distracted.\n");
113
+ const rl = await import("readline");
114
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
115
+ const answer = await new Promise(
116
+ (resolve) => iface.question("Keep waiting? (y/n): ", resolve)
117
+ );
118
+ iface.close();
119
+ if (answer.trim().toLowerCase().startsWith("y")) {
120
+ phase = "extended";
121
+ timeoutS = totalWaitedS + EXTEND_TIMEOUT_S;
122
+ console.log(`
123
+ Waiting ${EXTEND_TIMEOUT_S / 60} more minutes...`);
124
+ while (totalWaitedS < timeoutS) {
125
+ const minutesLeft = Math.ceil((timeoutS - totalWaitedS) / 60);
126
+ if (Math.floor(totalWaitedS) % 30 === 0 && totalWaitedS > INITIAL_TIMEOUT_S) {
127
+ process.stdout.write(`
128
+ Still waiting (${minutesLeft}m remaining)...`);
129
+ } else {
130
+ process.stdout.write(".");
131
+ }
132
+ await sleep(POLL_INTERVAL_MS);
133
+ totalWaitedS += POLL_INTERVAL_MS / 1e3;
134
+ try {
135
+ const checkRes = await fetch(`${API_BASE}/api/cli/auth?code=${code}`);
136
+ if (!checkRes.ok) continue;
137
+ const data = await checkRes.json();
138
+ if (data.status === "approved" && data.token) {
139
+ finishLogin(data);
140
+ return;
141
+ }
142
+ if (data.status === "expired") {
143
+ console.error("\n\nThe server-side login request expired. Run `npx intentdocs login` to start a new one.");
144
+ process.exit(1);
145
+ }
146
+ } catch {
147
+ }
148
+ }
149
+ }
150
+ }
151
+ console.error("\n\nLogin timed out. Run `npx intentdocs login` to try again.");
152
+ console.error("If the browser page is still open, complete it and re-run the command \u2014 your approval will still work within 10 minutes.\n");
153
+ process.exit(1);
154
+ }
155
+ async function logout() {
156
+ const { existsSync, unlinkSync } = await import("fs");
157
+ const { join } = await import("path");
158
+ const { homedir } = await import("os");
159
+ const authFile = join(homedir(), ".intent", "auth.json");
160
+ if (existsSync(authFile)) {
161
+ unlinkSync(authFile);
162
+ console.log("Logged out.");
163
+ } else {
164
+ console.log("Not logged in.");
165
+ }
166
+ }
167
+
168
+ export {
169
+ login,
170
+ logout
171
+ };
@@ -79,9 +79,56 @@ var CONTEXT_DIR = ".intent/stories";
79
79
  function storyContextPath(storyId) {
80
80
  return join(process.cwd(), CONTEXT_DIR, `${storyId}.md`);
81
81
  }
82
+ var HAND_WRITTEN_SECTIONS = {
83
+ "Approach": "_Plan your approach here",
84
+ "Implementation notes": "_Notes added via",
85
+ "Decisions": "_Log decisions via",
86
+ "Relevant files": "_Add file paths here"
87
+ };
88
+ var GENERATED_HEADINGS = /* @__PURE__ */ new Set([
89
+ "User story",
90
+ "Done when",
91
+ "Design references",
92
+ "Spec",
93
+ "Implementation Plan",
94
+ ...Object.keys(HAND_WRITTEN_SECTIONS)
95
+ ]);
96
+ function readHandWrittenSections(content) {
97
+ const body = content.replace(/\n---\s*\n\s*Mark done:[^\n]*\s*$/, "\n");
98
+ const sections = /* @__PURE__ */ new Map();
99
+ let current = null;
100
+ let buffer = [];
101
+ const flush = () => {
102
+ if (current && current in HAND_WRITTEN_SECTIONS) {
103
+ const placeholder = HAND_WRITTEN_SECTIONS[current];
104
+ const text = buffer.filter((line) => !line.trim().startsWith(placeholder)).join("\n").trim();
105
+ if (text) sections.set(current, text);
106
+ }
107
+ };
108
+ for (const line of body.split("\n")) {
109
+ const heading = line.match(/^## (.+?)\s*$/);
110
+ if (heading && GENERATED_HEADINGS.has(heading[1])) {
111
+ flush();
112
+ current = heading[1];
113
+ buffer = [];
114
+ continue;
115
+ }
116
+ if (current) buffer.push(line);
117
+ }
118
+ flush();
119
+ return sections;
120
+ }
82
121
  function writeStoryContext(story) {
83
122
  const dir = join(process.cwd(), CONTEXT_DIR);
84
123
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
124
+ const path = storyContextPath(story.id);
125
+ const kept = existsSync(path) ? readHandWrittenSections(readFileSync(path, "utf-8")) : /* @__PURE__ */ new Map();
126
+ const section = (heading, placeholder) => [
127
+ `## ${heading}`,
128
+ ``,
129
+ kept.get(heading) ?? placeholder,
130
+ ``
131
+ ];
85
132
  const acLines = story.acceptance_criteria.length ? story.acceptance_criteria.flatMap((ac) => renderCriterionLines(ac)).join("\n") : "_No acceptance criteria defined._";
86
133
  const specSection = story.notes?.trim() ? [`## Spec`, ``, story.notes.trim(), ``] : [];
87
134
  const designRefs = story.design_refs ?? [];
@@ -96,13 +143,10 @@ function writeStoryContext(story) {
96
143
  plan.markdown,
97
144
  ``,
98
145
  ...plan.files_to_create?.length ? [`### Files to create`, ``, ...plan.files_to_create.map((f) => `- \`${f}\``), ``] : [],
99
- ...plan.files_to_modify?.length ? [`### Files to modify`, ``, ...plan.files_to_modify.map((f) => `- \`${f}\``), ``] : []
100
- ] : [
101
- `## Approach`,
102
- ``,
103
- `_Plan your approach here before writing code._`,
104
- ``
105
- ];
146
+ ...plan.files_to_modify?.length ? [`### Files to modify`, ``, ...plan.files_to_modify.map((f) => `- \`${f}\``), ``] : [],
147
+ // An approach written before the plan arrived is still the developer's.
148
+ ...kept.has("Approach") ? section("Approach", "") : []
149
+ ] : section("Approach", `_Plan your approach here before writing code._`);
106
150
  const md = [
107
151
  `# ${story.title}`,
108
152
  ``,
@@ -122,23 +166,13 @@ function writeStoryContext(story) {
122
166
  ...designRefsSection,
123
167
  ...specSection,
124
168
  ...approachSection,
125
- `## Implementation notes`,
126
- ``,
127
- `_Notes added via \`npx intentdocs note ${storyRef(story)} "..."\`_`,
128
- ``,
129
- `## Decisions`,
130
- ``,
131
- `_Log decisions via \`npx intentdocs decide "..." -s ${storyRef(story)}\`_`,
132
- ``,
133
- `## Relevant files`,
134
- ``,
135
- `_Add file paths here as you explore the codebase._`,
136
- ``,
169
+ ...section("Implementation notes", `_Notes added via \`npx intentdocs note ${storyRef(story)} "..."\`_`),
170
+ ...section("Decisions", `_Log decisions via \`npx intentdocs decide "..." -s ${storyRef(story)}\`_`),
171
+ ...section("Relevant files", `_Add file paths here as you explore the codebase._`),
137
172
  `---`,
138
173
  ``,
139
174
  `Mark done: \`npx intentdocs done ${storyRef(story)}\``
140
175
  ].join("\n");
141
- const path = storyContextPath(story.id);
142
176
  writeFileSync(path, md);
143
177
  return path;
144
178
  }
@@ -0,0 +1,12 @@
1
+ // src/lib/notes.ts
2
+ function appendToNotes(existing, note) {
3
+ const before = existing?.trimEnd() ?? "";
4
+ const added = note.trim();
5
+ return before ? `${before}
6
+
7
+ ${added}` : added;
8
+ }
9
+
10
+ export {
11
+ appendToNotes
12
+ };
@@ -9,6 +9,14 @@ var AuthError = class extends Error {
9
9
  this.name = "AuthError";
10
10
  }
11
11
  };
12
+ var ApiError = class extends Error {
13
+ constructor(message, status, code = null) {
14
+ super(message);
15
+ this.status = status;
16
+ this.code = code;
17
+ this.name = "ApiError";
18
+ }
19
+ };
12
20
  function checkoutMismatch(config, binding) {
13
21
  const checkout = config.workspace;
14
22
  if (!checkout || !checkout.orgId) return null;
@@ -42,6 +50,17 @@ async function refusalMessage(res) {
42
50
  }
43
51
  return null;
44
52
  }
53
+ async function errorCode(res) {
54
+ try {
55
+ const body = await res.clone().json();
56
+ if (typeof body.error === "string") return body.error;
57
+ if (body.error && typeof body.error === "object" && typeof body.error.code === "string") {
58
+ return body.error.code;
59
+ }
60
+ } catch {
61
+ }
62
+ return null;
63
+ }
45
64
  async function apiFetch(config, path, init) {
46
65
  const auth = getAuth();
47
66
  const mismatch = checkoutMismatch(config, auth?.binding);
@@ -65,7 +84,10 @@ async function apiFetch(config, path, init) {
65
84
  said ?? "Access denied \u2014 you may not have permission for this project. Run `npx intentdocs login` to re-authenticate."
66
85
  );
67
86
  }
68
- if (!res.ok) throw new Error(`API error: ${res.status}`);
87
+ if (!res.ok) {
88
+ const said = await refusalMessage(res);
89
+ throw new ApiError(said ?? `API error: ${res.status}`, res.status, await errorCode(res));
90
+ }
69
91
  return res;
70
92
  }
71
93
  function apiUrl(config, path) {
@@ -154,6 +176,7 @@ async function retractDecision(config, decisionId, reason) {
154
176
  }
155
177
 
156
178
  export {
179
+ checkoutMismatch,
157
180
  syncHeaders,
158
181
  apiFetch,
159
182
  apiUrl,
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  getContext
3
- } from "./chunk-34JCN66H.js";
3
+ } from "./chunk-KTGQO3AE.js";
4
4
  import {
5
+ checkoutConfig,
6
+ hasConfig,
5
7
  writeConfig
6
- } from "./chunk-JX2RLGOM.js";
8
+ } from "./chunk-RGFGTIIH.js";
7
9
  import {
8
10
  ask,
9
11
  closePrompt,
@@ -20,8 +22,36 @@ import {
20
22
  // src/commands/scope.ts
21
23
  import { writeFileSync, existsSync } from "fs";
22
24
  import { join } from "path";
25
+ async function connectCheckout(apiBase, projectId) {
26
+ if (hasConfig()) {
27
+ console.log(" This checkout is already connected (.intent/config.json) \u2014 left as is.");
28
+ console.log(" Run `npx intentdocs sync` to refresh it, or remove .intent/config.json and run `npx intentdocs init` to connect it to this project.\n");
29
+ return null;
30
+ }
31
+ const mapsRes = await authenticatedFetch(`/api/cli/projects/${projectId}/maps`);
32
+ if (!mapsRes.ok) {
33
+ console.log(" \u26A0 Could not fetch the project's story map \u2014 run `npx intentdocs init` to connect.\n");
34
+ return null;
35
+ }
36
+ const { maps, appUrl } = await mapsRes.json();
37
+ const map = maps[0];
38
+ if (!map) {
39
+ console.log(" \u26A0 The project has no story map yet \u2014 run `npx intentdocs init` once it has one.\n");
40
+ return null;
41
+ }
42
+ let workspace = null;
43
+ const projectsRes = await authenticatedFetch("/api/cli/projects");
44
+ if (projectsRes.ok) {
45
+ const { projects } = await projectsRes.json();
46
+ workspace = projects.find((p) => p.id === projectId)?.workspace ?? null;
47
+ }
48
+ const config = checkoutConfig({ apiBase, project: { id: projectId, workspace }, map, appUrl });
49
+ writeConfig(config);
50
+ console.log(" \u2713 Connected\n");
51
+ return config;
52
+ }
23
53
  async function scope(projectId) {
24
- await requireValidAuth();
54
+ const auth = await requireValidAuth();
25
55
  if (!projectId) {
26
56
  projectId = await ask(" Project ID (from `intentdocs start`): ");
27
57
  }
@@ -112,18 +142,8 @@ async function scope(projectId) {
112
142
  console.log(" \u2713 Blueprint generated (couldn't parse summary)\n");
113
143
  }
114
144
  console.log("\n Connecting project to your codebase...");
115
- const mapsRes = await authenticatedFetch(
116
- `/api/storymapper/project/${projectId}`
117
- );
118
- let mapId = null;
119
- if (mapsRes.ok) {
120
- const mapData = await mapsRes.json();
121
- if (mapData.mapId) mapId = mapData.mapId;
122
- }
123
- if (mapId) {
124
- const config = { apiBase: API_BASE, projectId, mapId };
125
- writeConfig(config);
126
- console.log(" \u2713 Connected\n");
145
+ const config = await connectCheckout(auth.apiBase, projectId);
146
+ if (config) {
127
147
  try {
128
148
  const md = await getContext(config);
129
149
  writeFileSync(join(process.cwd(), "INTENT.md"), md);
@@ -176,7 +196,7 @@ async function scope(projectId) {
176
196
  " Configure your AI tool (Claude Code / Cursor) with intentdocs agent workflows?"
177
197
  );
178
198
  if (wantSetup) {
179
- const { setup } = await import("./setup-WSYIG2CR.js");
199
+ const { setup } = await import("./setup-W4PKG6GB.js");
180
200
  await setup();
181
201
  return;
182
202
  }
@@ -226,5 +246,6 @@ ${projectId}
226
246
  }
227
247
 
228
248
  export {
249
+ connectCheckout,
229
250
  scope
230
251
  };