@use-aistack/cli 0.2.0 → 0.4.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/dist/index.js CHANGED
@@ -3,14 +3,10 @@
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
5
 
6
- // src/commands/login.ts
7
- import * as p2 from "@clack/prompts";
8
- import open from "open";
9
-
10
6
  // src/api.ts
11
7
  var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
12
- async function request(path, options = {}) {
13
- return fetch(`${BASE_URL}${path}`, {
8
+ async function request(path2, options = {}) {
9
+ return fetch(`${BASE_URL}${path2}`, {
14
10
  ...options,
15
11
  headers: {
16
12
  "Content-Type": "application/json",
@@ -21,38 +17,45 @@ async function request(path, options = {}) {
21
17
  function authHeaders(token) {
22
18
  return { Authorization: `Bearer ${token}` };
23
19
  }
24
- async function authStart() {
25
- const res = await request("/api/cli/auth/start", { method: "POST" });
26
- if (!res.ok) throw new Error(`Auth start failed: ${res.status}`);
20
+ function failure(what, res) {
21
+ if (res.status === 429) {
22
+ const retry = res.headers.get("Retry-After");
23
+ return new Error(
24
+ retry ? `${what}: too many requests. Try again in ${retry} seconds.` : `${what}: too many requests. Try again in a minute.`
25
+ );
26
+ }
27
+ if (res.status === 403) {
28
+ return new Error(
29
+ `${what}: this machine is not allowed to do that. Run \`aistack login\` again to re-link it.`
30
+ );
31
+ }
32
+ return new Error(`${what}: ${res.status}`);
33
+ }
34
+ async function authStart(machineName) {
35
+ const res = await request("/api/cli/auth/start", {
36
+ method: "POST",
37
+ body: JSON.stringify(machineName ? { machineName } : {})
38
+ });
39
+ if (!res.ok) throw failure("Auth start failed", res);
27
40
  return res.json();
28
41
  }
29
42
  async function authPoll(secretId) {
30
43
  const res = await request(
31
44
  `/api/cli/auth/poll?secretId=${encodeURIComponent(secretId)}`
32
45
  );
33
- if (!res.ok) throw new Error(`Auth poll failed: ${res.status}`);
34
- return res.json();
35
- }
36
- async function projectsCheck(token, name) {
37
- const res = await request(
38
- `/api/cli/projects/check?name=${encodeURIComponent(name)}`,
39
- {
40
- headers: authHeaders(token)
41
- }
42
- );
43
- if (res.status === 401)
44
- throw new Error("Authentication expired. Run `aistack login` again.");
45
- if (!res.ok) throw new Error(`Project check failed: ${res.status}`);
46
+ if (!res.ok) throw failure("Auth poll failed", res);
46
47
  return res.json();
47
48
  }
48
- async function projectsCollect(token, data) {
49
- const res = await request("/api/cli/projects/collect", {
49
+ async function stackCollect(token, data) {
50
+ const res = await request("/api/cli/stacks/collect", {
50
51
  method: "POST",
51
52
  headers: authHeaders(token),
52
53
  body: JSON.stringify(data)
53
54
  });
54
55
  if (res.status === 401)
55
- throw new Error("Authentication expired. Run `aistack login` again.");
56
+ throw new Error(
57
+ "Authentication expired. Run `npx @use-aistack/cli login` again."
58
+ );
56
59
  if (!res.ok) {
57
60
  throw new Error(await formatHttpError(res, "Collect failed"));
58
61
  }
@@ -60,24 +63,122 @@ async function projectsCollect(token, data) {
60
63
  }
61
64
  async function formatHttpError(res, label) {
62
65
  const prefix = `${label}: ${res.status} ${res.statusText || ""}`.trim();
63
- const text2 = await res.text().catch(() => "");
64
- if (!text2) return prefix;
66
+ const text = await res.text().catch(() => "");
67
+ if (!text) return prefix;
65
68
  try {
66
- const body = JSON.parse(text2);
69
+ const body = JSON.parse(text);
67
70
  const detail = body.error || body.message;
68
71
  if (detail) return `${prefix} \u2014 ${detail}`;
69
72
  } catch {
70
73
  }
71
- const snippet = text2.trim().slice(0, 500);
74
+ const snippet = text.trim().slice(0, 500);
72
75
  return snippet ? `${prefix} \u2014 ${snippet}` : prefix;
73
76
  }
74
- async function projectGet(shortId) {
75
- const res = await request(`/api/cli/projects/${encodeURIComponent(shortId)}`);
77
+ async function syncPublish(token, bodyJson) {
78
+ const res = await request("/api/cli/sync", {
79
+ method: "POST",
80
+ headers: authHeaders(token),
81
+ body: bodyJson
82
+ });
83
+ if (res.status === 401)
84
+ throw new Error(
85
+ "Authentication expired. Run `npx @use-aistack/cli login` again."
86
+ );
87
+ if (res.status === 403 || res.status === 429)
88
+ throw failure("Sync failed", res);
89
+ if (!res.ok) {
90
+ throw new Error(await formatHttpError(res, "Sync failed"));
91
+ }
92
+ return res.json();
93
+ }
94
+ async function stackGet(token) {
95
+ const res = await request("/api/cli/stacks", {
96
+ headers: authHeaders(token)
97
+ });
98
+ if (res.status === 401)
99
+ throw new Error(
100
+ "Authentication expired. Run `npx @use-aistack/cli login` again."
101
+ );
76
102
  if (res.status === 404) return null;
77
- if (!res.ok) throw new Error(`Project fetch failed: ${res.status}`);
103
+ if (!res.ok) throw failure("Stack fetch failed", res);
78
104
  return res.json();
79
105
  }
80
106
 
107
+ // src/commands/collect.ts
108
+ import * as p2 from "@clack/prompts";
109
+
110
+ // src/classifier.ts
111
+ import { basename, dirname } from "path";
112
+
113
+ // src/stableKey.ts
114
+ function computeStableKey(group, type, relPath) {
115
+ return `${group}:${type}:${relPath}`;
116
+ }
117
+
118
+ // src/classifier.ts
119
+ function classify(files) {
120
+ const groups = /* @__PURE__ */ new Map();
121
+ const singletons = [];
122
+ const singletonRoots = /* @__PURE__ */ new Set([
123
+ ".",
124
+ "~",
125
+ "~/.claude",
126
+ "~/.cursor",
127
+ "~/.continue",
128
+ ".claude",
129
+ ".cursor",
130
+ ".github"
131
+ ]);
132
+ for (const file of files) {
133
+ const dir = dirname(file.relativePath);
134
+ const isSingleton = singletonRoots.has(dir);
135
+ if (isSingleton) {
136
+ singletons.push(file);
137
+ } else {
138
+ const key = `${file.group}:${file.source}:${file.type}:${dir}`;
139
+ const existing = groups.get(key) ?? [];
140
+ existing.push(file);
141
+ groups.set(key, existing);
142
+ }
143
+ }
144
+ const items = [];
145
+ for (const file of singletons) {
146
+ const relPath = file.relativePath.replace(/^~\/\.[^/]+\//, "").replace(/^\.[^/]+\//, "");
147
+ items.push({
148
+ type: file.type,
149
+ name: file.relativePath,
150
+ group: file.group,
151
+ stableKey: computeStableKey(file.group, file.type, relPath),
152
+ files: [
153
+ {
154
+ name: basename(file.relativePath),
155
+ content: file.content,
156
+ path: file.relativePath
157
+ }
158
+ ]
159
+ });
160
+ }
161
+ for (const [, groupFiles] of groups) {
162
+ const first = groupFiles[0];
163
+ const dir = dirname(first.relativePath);
164
+ const relPath = dir.replace(/^~\/\.claude\//, "").replace(/^\.claude\//, "").replace(/^~\/\.cursor\//, "").replace(/^\.cursor\//, "");
165
+ const typeLabel = first.type === "subagent" ? "subagents" : `${first.type}s`;
166
+ items.push({
167
+ type: first.type,
168
+ name: dir,
169
+ description: `${groupFiles.length} ${typeLabel}`,
170
+ group: first.group,
171
+ stableKey: computeStableKey(first.group, first.type, relPath),
172
+ files: groupFiles.map((f) => ({
173
+ name: basename(f.relativePath),
174
+ content: f.content,
175
+ path: f.relativePath
176
+ }))
177
+ });
178
+ }
179
+ return items;
180
+ }
181
+
81
182
  // src/config.ts
82
183
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
83
184
  import { homedir } from "os";
@@ -99,6 +200,23 @@ function saveToken(token, userId) {
99
200
  mkdirSync(CONFIG_DIR, { recursive: true });
100
201
  writeFileSync(CREDENTIALS_FILE, JSON.stringify({ token, userId }, null, 2));
101
202
  }
203
+ var SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
204
+ function getSettings() {
205
+ if (!existsSync(SETTINGS_FILE)) return {};
206
+ try {
207
+ const raw = JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
208
+ return raw && typeof raw === "object" ? raw : {};
209
+ } catch {
210
+ return {};
211
+ }
212
+ }
213
+ function saveSettings(patch) {
214
+ mkdirSync(CONFIG_DIR, { recursive: true });
215
+ writeFileSync(
216
+ SETTINGS_FILE,
217
+ JSON.stringify({ ...getSettings(), ...patch }, null, 2)
218
+ );
219
+ }
102
220
  var PROJECTS_FILE = join(CONFIG_DIR, "projects.json");
103
221
  function readProjects() {
104
222
  if (!existsSync(PROJECTS_FILE)) return {};
@@ -107,9 +225,10 @@ function readProjects() {
107
225
  const data = {};
108
226
  for (const [key, value] of Object.entries(raw)) {
109
227
  if (typeof value === "string") {
110
- data[key] = { name: value };
111
- } else {
112
- data[key] = value;
228
+ data[key] = {};
229
+ } else if (value && typeof value === "object") {
230
+ const excluded = value.excluded;
231
+ data[key] = Array.isArray(excluded) ? { excluded } : {};
113
232
  }
114
233
  }
115
234
  return data;
@@ -121,160 +240,486 @@ function writeProjects(data) {
121
240
  mkdirSync(CONFIG_DIR, { recursive: true });
122
241
  writeFileSync(PROJECTS_FILE, JSON.stringify(data, null, 2));
123
242
  }
124
- function getProjectName(directory) {
125
- return readProjects()[directory]?.name ?? null;
126
- }
127
243
  function getExcludedPaths(directory) {
128
244
  return readProjects()[directory]?.excluded ?? [];
129
245
  }
130
- function saveProjectSettings(directory, name, excluded) {
246
+ function saveExcludedPaths(directory, excluded) {
131
247
  const data = readProjects();
132
248
  data[directory] = {
133
- name,
134
249
  excluded: excluded.length > 0 ? excluded : void 0
135
250
  };
136
251
  writeProjects(data);
137
252
  }
138
253
 
139
- // src/theme.ts
140
- import * as p from "@clack/prompts";
141
- var esc = (code) => `\x1B[${code}m`;
142
- var reset = esc("0");
143
- var LIME = "163;230;53";
144
- var BLACK = "0;0;0";
145
- var YELLOW = "250;204;21";
146
- var RED = "248;113;113";
147
- var MUTED = "120;120;120";
148
- var lime = (s) => `${esc(`38;2;${LIME}`)}${s}${reset}`;
149
- var limeBold = (s) => `${esc("1")}${esc(`38;2;${LIME}`)}${s}${reset}`;
150
- var bgLime = (s) => `${esc(`48;2;${LIME}`)}${esc(`38;2;${BLACK}`)}${s}${reset}`;
151
- var yellow = (s) => `${esc(`38;2;${YELLOW}`)}${s}${reset}`;
152
- var red = (s) => `${esc(`38;2;${RED}`)}${s}${reset}`;
153
- var dim = (s) => `${esc(`38;2;${MUTED}`)}${s}${reset}`;
154
- var bold = (s) => `${esc("1")}${s}${reset}`;
155
- var banner = (cmd) => `${lime("\u25A0")} ${bgLime(` AISTACK `)} ${bold(cmd.toUpperCase())}`;
156
- var BAR = `${esc(`38;2;${MUTED}`)}\u2502${reset}`;
157
- function lines(items) {
158
- for (const item of items) {
159
- console.log(`${BAR} ${item}`);
160
- }
254
+ // src/git.ts
255
+ import { execFileSync } from "child_process";
256
+
257
+ // src/github-repo.ts
258
+ function isGithubHost(host) {
259
+ const h = host.toLowerCase();
260
+ return h === "github.com" || h === "www.github.com";
161
261
  }
162
- function section(label, count) {
163
- console.log(`${BAR}`);
164
- const countStr = count !== void 0 ? ` ${dim(String(count))}` : "";
165
- console.log(`${BAR} ${bold(label.toUpperCase())}${countStr}`);
262
+ function parseRepo(input) {
263
+ const trimmed = input.trim();
264
+ if (!trimmed) return null;
265
+ const scpMatch = trimmed.match(/^[^@]+@([^:]+):(.+)$/);
266
+ let host;
267
+ let withoutHost;
268
+ if (scpMatch) {
269
+ host = scpMatch[1];
270
+ withoutHost = scpMatch[2];
271
+ } else {
272
+ const withoutScheme = trimmed.replace(/^[a-z]+:\/\//i, "");
273
+ const slash = withoutScheme.indexOf("/");
274
+ if (slash === -1) return null;
275
+ host = withoutScheme.slice(0, slash);
276
+ withoutHost = withoutScheme.slice(slash + 1);
277
+ }
278
+ if (!isGithubHost(host)) return null;
279
+ const pathPart = withoutHost.replace(/[?#].*$/, "");
280
+ const segments = pathPart.split("/").filter(Boolean);
281
+ const owner = segments[0];
282
+ const repo = segments[1]?.replace(/\.git$/, "");
283
+ if (!owner || !repo) return null;
284
+ return { owner: owner.toLowerCase(), repo: repo.toLowerCase() };
166
285
  }
167
- function divider() {
168
- console.log(`${BAR} ${dim("\u2500".repeat(40))}`);
286
+ function canonicalizeRepoUrl(input) {
287
+ const parsed = parseRepo(input);
288
+ if (!parsed) return null;
289
+ return `https://github.com/${parsed.owner}/${parsed.repo}`;
169
290
  }
170
- function intro2(cmd) {
171
- console.log();
172
- p.intro(banner(cmd));
291
+ function repoNameFromCanonical(canonical) {
292
+ return parseRepo(canonical)?.repo ?? "";
173
293
  }
174
- function outro2(msg) {
175
- p.outro(msg);
176
- console.log();
294
+ function normalizeUpstreamPath(path2) {
295
+ if (!path2) return "";
296
+ return path2.split("/").filter(Boolean).join("/");
177
297
  }
178
- function outroError(msg) {
179
- p.outro(red(msg));
180
- console.log();
298
+
299
+ // src/git.ts
300
+ var defaultGitRemoteRunner = (cwd) => {
301
+ try {
302
+ return execFileSync("git", ["-C", cwd, "remote", "get-url", "origin"], {
303
+ encoding: "utf-8",
304
+ stdio: ["ignore", "pipe", "ignore"]
305
+ }).trim();
306
+ } catch {
307
+ return null;
308
+ }
309
+ };
310
+ function detectRepoUrl(cwd, run = defaultGitRemoteRunner) {
311
+ const raw = run(cwd);
312
+ if (!raw) return null;
313
+ return canonicalizeRepoUrl(raw);
181
314
  }
182
- function outroCancel(msg = "cancelled") {
183
- p.cancel(dim(msg));
184
- console.log();
315
+ function buildLinkResource(spec) {
316
+ const normPath = normalizeUpstreamPath(spec.path);
317
+ return {
318
+ type: spec.type,
319
+ name: spec.name,
320
+ group: spec.group,
321
+ stableKey: `linked:${spec.canonical}:${normPath}`,
322
+ upstream: {
323
+ repoUrl: spec.canonical,
324
+ ...normPath ? { path: normPath } : {},
325
+ ...spec.sha ? { lastCommitSha: spec.sha } : {}
326
+ }
327
+ };
185
328
  }
186
- function outroSkipped(msg) {
187
- p.outro(dim(msg));
188
- console.log();
329
+ function buildRepoLinkResource(canonical) {
330
+ return buildLinkResource({
331
+ canonical,
332
+ name: repoNameFromCanonical(canonical),
333
+ type: "custom",
334
+ group: "generic"
335
+ });
189
336
  }
190
337
 
191
- // src/commands/login.ts
192
- async function loginCommand() {
193
- intro2("login");
194
- const s = p2.spinner();
195
- s.start("Starting authentication...");
196
- let session;
197
- try {
198
- session = await authStart();
199
- s.stop("Session created");
200
- } catch (err) {
201
- s.stop("Failed to start authentication");
202
- p2.log.error(err instanceof Error ? err.message : String(err));
203
- outroError("error");
204
- process.exit(1);
205
- }
206
- p2.log.info(`${dim("CODE")} ${limeBold(session.userCode)}`);
207
- p2.log.info(`${dim("OPEN")} ${dim(session.authUrl)}`);
338
+ // src/hooks.ts
339
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
340
+ import { homedir as homedir2 } from "os";
341
+ import { join as join2 } from "path";
342
+ function readJson(path2) {
208
343
  try {
209
- await open(session.authUrl);
344
+ if (!existsSync2(path2)) return null;
345
+ return JSON.parse(readFileSync2(path2, "utf-8"));
210
346
  } catch {
211
- p2.log.warn(
212
- "Could not open browser automatically. Please visit the URL above."
213
- );
347
+ return null;
214
348
  }
215
- s.start("Waiting for approval...");
216
- const maxAttempts = 36;
217
- for (let i = 0; i < maxAttempts; i++) {
218
- await new Promise((resolve) => setTimeout(resolve, 5e3));
219
- try {
220
- const result = await authPoll(session.secretId);
221
- if (result.status === "approved" && result.token) {
222
- s.stop(lime("Authenticated"));
223
- saveToken(result.token, result.userId);
224
- p2.log.success(
225
- `Token saved. Run ${limeBold("aistack collect")} to get started.`
226
- );
227
- outro2(lime("done"));
228
- return;
229
- }
230
- if (result.status === "expired") {
231
- s.stop("Session expired");
232
- p2.log.error("Authentication session expired. Please try again.");
233
- outroError("expired");
234
- process.exit(1);
235
- }
236
- } catch (err) {
237
- s.stop("Error polling");
238
- p2.log.error(err instanceof Error ? err.message : String(err));
239
- outroError("error");
240
- process.exit(1);
241
- }
349
+ }
350
+ function hooksFrom(path2, source, out, seen) {
351
+ const hooks = readJson(path2)?.hooks;
352
+ if (!hooks || typeof hooks !== "object") return;
353
+ for (const [event, config] of Object.entries(hooks)) {
354
+ const stableKey = `hooks:${source}:${event}`;
355
+ if (seen.has(stableKey)) continue;
356
+ seen.add(stableKey);
357
+ out.push({
358
+ type: "hook",
359
+ name: event,
360
+ group: "claude-code",
361
+ stableKey,
362
+ files: [
363
+ {
364
+ name: `${event}.json`,
365
+ content: JSON.stringify(config, null, 2),
366
+ path: `hooks/${event}.json`
367
+ }
368
+ ]
369
+ });
242
370
  }
243
- s.stop("Timed out");
244
- p2.log.error("Authentication timed out after 3 minutes. Please try again.");
245
- outroError("timed out");
246
- process.exit(1);
371
+ }
372
+ function detectHooks(cwd, home = homedir2()) {
373
+ const out = [];
374
+ const seen = /* @__PURE__ */ new Set();
375
+ hooksFrom(join2(cwd, ".claude", "settings.json"), "local", out, seen);
376
+ hooksFrom(join2(cwd, ".claude", "settings.local.json"), "local", out, seen);
377
+ hooksFrom(join2(home, ".claude", "settings.json"), "global", out, seen);
378
+ return out;
247
379
  }
248
380
 
249
- // src/commands/collect.ts
250
- import * as p3 from "@clack/prompts";
251
- import { basename as basename2 } from "path";
252
-
253
- // src/scanner.ts
254
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
255
- import { join as join2, relative } from "path";
256
- import { homedir as homedir2 } from "os";
257
- import ignore from "ignore";
258
- var MAX_FILE_SIZE = 100 * 1024;
259
- var LOCAL_PATTERNS = [
260
- // Rules
261
- { path: "CLAUDE.md", type: "rule", group: "claude-code" },
262
- { path: "AGENTS.md", type: "rule", group: "claude-code" },
263
- { path: ".cursorrules", type: "rule", group: "cursor" },
264
- { path: ".windsurfrules", type: "rule", group: "windsurf" },
265
- { path: ".clinerules", type: "rule", group: "cline" },
266
- { path: ".github/copilot-instructions.md", type: "rule", group: "copilot" },
267
- // MCP
268
- { path: "mcp.json", type: "mcp", group: "generic" },
269
- { path: ".cursor/mcp.json", type: "mcp", group: "cursor" },
270
- {
271
- path: "claude_desktop_config.json",
381
+ // src/mcp.ts
382
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
383
+ import { homedir as homedir3, platform } from "os";
384
+ import { join as join3 } from "path";
385
+ import { parse as parseToml } from "smol-toml";
386
+ import { parse as parseYaml } from "yaml";
387
+ function commandName(cmd) {
388
+ return cmd.replace(/\\/g, "/").split("/").pop() ?? cmd;
389
+ }
390
+ function splitVersion(spec) {
391
+ const at = spec.indexOf("@", spec.startsWith("@") ? 1 : 0);
392
+ if (at <= 0) return { id: spec };
393
+ return { id: spec.slice(0, at), version: spec.slice(at + 1) || void 0 };
394
+ }
395
+ function firstPositional(args, skip = 0) {
396
+ for (const a of args.slice(skip)) {
397
+ if (!a.startsWith("-")) return a;
398
+ }
399
+ return void 0;
400
+ }
401
+ var CONTAINER_VALUE_FLAGS = /* @__PURE__ */ new Set([
402
+ "-e",
403
+ "--env",
404
+ "-v",
405
+ "--volume",
406
+ "-p",
407
+ "--publish",
408
+ "-w",
409
+ "--workdir",
410
+ "--name",
411
+ "--mount",
412
+ "--network",
413
+ "-u",
414
+ "--user",
415
+ "-l",
416
+ "--label"
417
+ ]);
418
+ function containerImage(args) {
419
+ const runIdx = args.indexOf("run");
420
+ const rest = runIdx >= 0 ? args.slice(runIdx + 1) : args;
421
+ for (let i = 0; i < rest.length; i++) {
422
+ const a = rest[i];
423
+ if (a.startsWith("-")) {
424
+ if (CONTAINER_VALUE_FLAGS.has(a) && !a.includes("=")) i++;
425
+ continue;
426
+ }
427
+ return a;
428
+ }
429
+ return void 0;
430
+ }
431
+ function splitImageTag(image) {
432
+ const colon = image.lastIndexOf(":");
433
+ if (colon > 0 && !image.slice(colon + 1).includes("/")) {
434
+ return { id: image.slice(0, colon), version: image.slice(colon + 1) };
435
+ }
436
+ return { id: image };
437
+ }
438
+ function parseMcpPackage(server) {
439
+ if (server.url) {
440
+ const t = (server.type ?? server.transport ?? "").toLowerCase();
441
+ return {
442
+ registry: "url",
443
+ id: server.url,
444
+ transport: t === "sse" ? "sse" : "http"
445
+ };
446
+ }
447
+ const command = server.command ? commandName(server.command) : "";
448
+ if (!command) return null;
449
+ const args = server.args ?? [];
450
+ if (command === "npx" || command === "bunx" || command === "pnpx") {
451
+ const spec = firstPositional(args);
452
+ return spec ? { registry: "npm", ...splitVersion(spec), transport: "stdio" } : null;
453
+ }
454
+ if ((command === "pnpm" || command === "yarn") && args[0] === "dlx") {
455
+ const spec = firstPositional(args, 1);
456
+ return spec ? { registry: "npm", ...splitVersion(spec), transport: "stdio" } : null;
457
+ }
458
+ if (command === "uvx") {
459
+ const spec = firstPositional(args);
460
+ return spec ? { registry: "pypi", ...splitVersion(spec), transport: "stdio" } : null;
461
+ }
462
+ if (command === "pipx" && args[0] === "run") {
463
+ const spec = firstPositional(args, 1);
464
+ return spec ? { registry: "pypi", ...splitVersion(spec), transport: "stdio" } : null;
465
+ }
466
+ if (command === "uv" && args[0] === "tool" && args[1] === "run") {
467
+ const spec = firstPositional(args, 2);
468
+ return spec ? { registry: "pypi", ...splitVersion(spec), transport: "stdio" } : null;
469
+ }
470
+ if (/^python[0-9.]*$/.test(command)) {
471
+ const i = args.indexOf("-m");
472
+ const mod = i >= 0 ? args[i + 1] : void 0;
473
+ return mod ? { registry: "pypi", id: mod, transport: "stdio" } : null;
474
+ }
475
+ if (command === "docker" || command === "podman") {
476
+ const image = containerImage(args);
477
+ if (!image) return null;
478
+ return { registry: "oci", ...splitImageTag(image), transport: "stdio" };
479
+ }
480
+ return null;
481
+ }
482
+ function buildMcpResource(name, group, pkg) {
483
+ return {
272
484
  type: "mcp",
273
- group: "claude-desktop"
274
- },
485
+ name,
486
+ group,
487
+ stableKey: `linked:pkg:${pkg.registry}:${pkg.id}`,
488
+ pkg
489
+ };
490
+ }
491
+ function readText(path2) {
492
+ try {
493
+ if (!existsSync3(path2)) return null;
494
+ return readFileSync3(path2, "utf-8");
495
+ } catch {
496
+ return null;
497
+ }
498
+ }
499
+ function readParsed(path2, parse) {
500
+ const raw = readText(path2);
501
+ if (raw === null) return null;
502
+ try {
503
+ return parse(raw);
504
+ } catch {
505
+ return null;
506
+ }
507
+ }
508
+ function readJson2(path2) {
509
+ return readParsed(path2, JSON.parse);
510
+ }
511
+ function readYaml(path2) {
512
+ return readParsed(path2, parseYaml);
513
+ }
514
+ function readToml(path2) {
515
+ return readParsed(path2, parseToml);
516
+ }
517
+ function continueListToMap(file) {
518
+ if (!file?.mcpServers?.length) return void 0;
519
+ const map = {};
520
+ file.mcpServers.forEach((s, i) => {
521
+ map[s.name ?? `server-${i}`] = s;
522
+ });
523
+ return map;
524
+ }
525
+ function vscodeGlobalStorageBases(home) {
526
+ const apps = ["Code", "Code - OSS", "VSCodium"];
527
+ let root;
528
+ if (platform() === "darwin") {
529
+ root = join3(home, "Library", "Application Support");
530
+ } else if (platform() === "win32") {
531
+ root = process.env.APPDATA ?? join3(home, "AppData", "Roaming");
532
+ } else {
533
+ root = process.env.XDG_CONFIG_HOME ?? join3(home, ".config");
534
+ }
535
+ return apps.map((app) => join3(root, app, "User", "globalStorage"));
536
+ }
537
+ function detectMcpServers(cwd, home = homedir3()) {
538
+ const out = [];
539
+ const seen = /* @__PURE__ */ new Set();
540
+ const add = (servers, group) => {
541
+ for (const [name, cfg] of Object.entries(servers ?? {})) {
542
+ const pkg = parseMcpPackage(cfg);
543
+ if (!pkg) continue;
544
+ const resource = buildMcpResource(name, group, pkg);
545
+ if (seen.has(resource.stableKey)) continue;
546
+ seen.add(resource.stableKey);
547
+ out.push(resource);
548
+ }
549
+ };
550
+ add(readJson2(join3(cwd, ".mcp.json"))?.mcpServers, "claude-code");
551
+ add(readJson2(join3(cwd, "mcp.json"))?.mcpServers, "generic");
552
+ add(
553
+ readJson2(join3(cwd, ".cursor", "mcp.json"))?.mcpServers,
554
+ "cursor"
555
+ );
556
+ add(readJson2(join3(cwd, ".vscode", "mcp.json"))?.servers, "generic");
557
+ add(
558
+ readJson2(join3(cwd, "claude_desktop_config.json"))?.mcpServers,
559
+ "claude-desktop"
560
+ );
561
+ for (const file of listYamlFiles(join3(cwd, ".continue", "mcpServers"))) {
562
+ add(continueListToMap(readYaml(file)), "continue");
563
+ }
564
+ add(readJson2(join3(cwd, ".roo", "mcp.json"))?.mcpServers, "roo");
565
+ const claudeJson = readJson2(join3(home, ".claude.json"));
566
+ add(claudeJson?.projects?.[cwd]?.mcpServers, "claude-code");
567
+ add(claudeJson?.mcpServers, "claude-code");
568
+ add(
569
+ readJson2(join3(home, ".cursor", "mcp.json"))?.mcpServers,
570
+ "cursor"
571
+ );
572
+ add(
573
+ readJson2(join3(home, ".codeium", "windsurf", "mcp_config.json"))?.mcpServers,
574
+ "windsurf"
575
+ );
576
+ for (const base of vscodeGlobalStorageBases(home)) {
577
+ add(
578
+ readJson2(
579
+ join3(
580
+ base,
581
+ "saoudrizwan.claude-dev",
582
+ "settings",
583
+ "cline_mcp_settings.json"
584
+ )
585
+ )?.mcpServers,
586
+ "cline"
587
+ );
588
+ add(
589
+ readJson2(
590
+ join3(
591
+ base,
592
+ "rooveterinaryinc.roo-cline",
593
+ "settings",
594
+ "mcp_settings.json"
595
+ )
596
+ )?.mcpServers,
597
+ "roo"
598
+ );
599
+ }
600
+ for (const file of listYamlFiles(join3(home, ".continue", "mcpServers"))) {
601
+ add(continueListToMap(readYaml(file)), "continue");
602
+ }
603
+ add(
604
+ readJson2(join3(home, ".gemini", "settings.json"))?.mcpServers,
605
+ "gemini"
606
+ );
607
+ add(
608
+ readToml(join3(home, ".codex", "config.toml"))?.mcp_servers,
609
+ "codex"
610
+ );
611
+ return out;
612
+ }
613
+ function listYamlFiles(dir) {
614
+ try {
615
+ return readdirSync(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => join3(dir, f));
616
+ } catch {
617
+ return [];
618
+ }
619
+ }
620
+
621
+ // src/plugins.ts
622
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
623
+ import { homedir as homedir4 } from "os";
624
+ import { join as join4 } from "path";
625
+ function marketplaceRepoUrl(mp) {
626
+ const src = mp?.source;
627
+ if (!src) return null;
628
+ if (src.repo) return `https://github.com/${src.repo}`;
629
+ return src.url ?? null;
630
+ }
631
+ function resolveSource(entry, mpRepoUrl) {
632
+ const src = entry.source;
633
+ if (typeof src === "string") {
634
+ if (!mpRepoUrl) return null;
635
+ const path2 = src.replace(/^\.\//, "").replace(/\/+$/, "");
636
+ return { url: mpRepoUrl, path: path2 || void 0 };
637
+ }
638
+ if (src && typeof src === "object" && src.url) {
639
+ return { url: src.url, path: src.path, sha: src.sha };
640
+ }
641
+ const fallback = entry.repository ?? entry.homepage ?? mpRepoUrl;
642
+ return fallback ? { url: fallback } : null;
643
+ }
644
+ function resolvePluginLinks(installed, marketplaces, manifests) {
645
+ const out = [];
646
+ for (const [key, entries] of Object.entries(installed.plugins ?? {})) {
647
+ const at = key.lastIndexOf("@");
648
+ if (at <= 0) continue;
649
+ const pluginName = key.slice(0, at);
650
+ const marketplace = key.slice(at + 1);
651
+ const mpRepoUrl = marketplaceRepoUrl(marketplaces[marketplace]);
652
+ const entry = manifests[marketplace]?.plugins?.find(
653
+ (p7) => p7.name === pluginName
654
+ );
655
+ if (!entry) continue;
656
+ const resolved = resolveSource(entry, mpRepoUrl);
657
+ if (!resolved) continue;
658
+ const canonical = canonicalizeRepoUrl(resolved.url);
659
+ if (!canonical) continue;
660
+ out.push(
661
+ buildLinkResource({
662
+ canonical,
663
+ path: resolved.path,
664
+ name: pluginName,
665
+ type: "plugin",
666
+ group: "claude-code",
667
+ sha: resolved.sha ?? entries[0]?.gitCommitSha
668
+ })
669
+ );
670
+ }
671
+ return out;
672
+ }
673
+ function readJson3(path2) {
674
+ try {
675
+ if (!existsSync4(path2)) return null;
676
+ return JSON.parse(readFileSync4(path2, "utf-8"));
677
+ } catch {
678
+ return null;
679
+ }
680
+ }
681
+ function detectInstalledPlugins(pluginsDir = join4(homedir4(), ".claude", "plugins")) {
682
+ const installed = readJson3(
683
+ join4(pluginsDir, "installed_plugins.json")
684
+ );
685
+ if (!installed?.plugins) return [];
686
+ const marketplaces = readJson3(join4(pluginsDir, "known_marketplaces.json")) ?? {};
687
+ const manifests = {};
688
+ for (const key of Object.keys(installed.plugins)) {
689
+ const mp = key.slice(key.lastIndexOf("@") + 1);
690
+ if (!mp || manifests[mp]) continue;
691
+ const installLocation = marketplaces[mp]?.installLocation ?? join4(pluginsDir, "marketplaces", mp);
692
+ const manifest = readJson3(
693
+ join4(installLocation, ".claude-plugin", "marketplace.json")
694
+ );
695
+ if (manifest) manifests[mp] = manifest;
696
+ }
697
+ return resolvePluginLinks(installed, marketplaces, manifests);
698
+ }
699
+
700
+ // src/scanner.ts
701
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync } from "fs";
702
+ import { homedir as homedir5 } from "os";
703
+ import { join as join5, relative } from "path";
704
+ import ignore from "ignore";
705
+ var MAX_FILE_SIZE = 100 * 1024;
706
+ var LOCAL_PATTERNS = [
707
+ // Rules
708
+ { path: "CLAUDE.md", type: "rule", group: "claude-code" },
709
+ { path: "AGENTS.md", type: "rule", group: "claude-code" },
710
+ { path: "GEMINI.md", type: "rule", group: "gemini" },
711
+ { path: ".cursorrules", type: "rule", group: "cursor" },
712
+ { path: ".windsurfrules", type: "rule", group: "windsurf" },
713
+ { path: ".clinerules", type: "rule", group: "cline" },
714
+ { path: ".roorules", type: "rule", group: "roo" },
715
+ { path: ".github/copilot-instructions.md", type: "rule", group: "copilot" },
716
+ // MCP servers are detected separately as pkg-reference links (see mcp.ts) —
717
+ // their config files are intentionally NOT collected as content here (which
718
+ // would also upload `env` secrets).
275
719
  // Config
276
720
  { path: ".aider.conf.yml", type: "config", group: "aider" },
277
721
  { path: ".continue/config.json", type: "config", group: "continue" },
722
+ { path: ".continue/config.yaml", type: "config", group: "continue" },
278
723
  { path: ".claude/settings.json", type: "config", group: "claude-code" },
279
724
  {
280
725
  path: ".claude/settings.local.json",
@@ -286,6 +731,11 @@ var LOCAL_PATTERNS = [
286
731
  ];
287
732
  var LOCAL_DIR_PATTERNS = [
288
733
  { dir: ".cursor/rules", type: "rule", group: "cursor" },
734
+ { dir: ".clinerules", type: "rule", group: "cline" },
735
+ { dir: ".windsurf/rules", type: "rule", group: "windsurf" },
736
+ { dir: ".roo/rules", type: "rule", group: "roo" },
737
+ { dir: ".github/instructions", type: "rule", group: "copilot" },
738
+ { dir: ".github/prompts", type: "prompt", group: "copilot" },
289
739
  { dir: ".claude/commands", type: "command", group: "claude-code" },
290
740
  { dir: ".claude/agents", type: "subagent", group: "claude-code" },
291
741
  { dir: ".claude/hooks", type: "hook", group: "claude-code" },
@@ -294,28 +744,28 @@ var LOCAL_DIR_PATTERNS = [
294
744
  ];
295
745
  function loadGitignore(cwd) {
296
746
  const ig = ignore();
297
- const gitignorePath = join2(cwd, ".gitignore");
298
- if (existsSync2(gitignorePath)) {
299
- ig.add(readFileSync2(gitignorePath, "utf-8"));
747
+ const gitignorePath = join5(cwd, ".gitignore");
748
+ if (existsSync5(gitignorePath)) {
749
+ ig.add(readFileSync5(gitignorePath, "utf-8"));
300
750
  }
301
751
  ig.add(["node_modules", ".git", "dist", "build", ".next", ".output"]);
302
752
  return ig;
303
753
  }
304
754
  function readFileSafe(filePath) {
305
755
  try {
306
- const stat = statSync(filePath);
307
- if (stat.size > MAX_FILE_SIZE) return null;
308
- return readFileSync2(filePath, "utf-8");
756
+ const stat2 = statSync(filePath);
757
+ if (stat2.size > MAX_FILE_SIZE) return null;
758
+ return readFileSync5(filePath, "utf-8");
309
759
  } catch {
310
760
  return null;
311
761
  }
312
762
  }
313
763
  function walkDir(dir, maxDepth = 3, currentDepth = 0) {
314
- if (currentDepth >= maxDepth || !existsSync2(dir)) return [];
764
+ if (currentDepth >= maxDepth || !existsSync5(dir)) return [];
315
765
  const results = [];
316
766
  try {
317
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
318
- const fullPath = join2(dir, entry.name);
767
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
768
+ const fullPath = join5(dir, entry.name);
319
769
  if (entry.isFile()) {
320
770
  results.push(fullPath);
321
771
  } else if (entry.isDirectory()) {
@@ -330,7 +780,7 @@ function scanLocal(cwd) {
330
780
  const ig = loadGitignore(cwd);
331
781
  const results = [];
332
782
  for (const pattern of LOCAL_PATTERNS) {
333
- const filePath = join2(cwd, pattern.path);
783
+ const filePath = join5(cwd, pattern.path);
334
784
  const content = readFileSafe(filePath);
335
785
  if (content !== null) {
336
786
  const rel = relative(cwd, filePath);
@@ -347,7 +797,7 @@ function scanLocal(cwd) {
347
797
  }
348
798
  }
349
799
  for (const { dir, type, group } of LOCAL_DIR_PATTERNS) {
350
- const dirPath = join2(cwd, dir);
800
+ const dirPath = join5(cwd, dir);
351
801
  const files = walkDir(dirPath);
352
802
  for (const filePath of files) {
353
803
  const rel = relative(cwd, filePath);
@@ -366,11 +816,11 @@ function scanLocal(cwd) {
366
816
  }
367
817
  }
368
818
  try {
369
- for (const entry of readdirSync(cwd, { withFileTypes: true }).filter(
819
+ for (const entry of readdirSync2(cwd, { withFileTypes: true }).filter(
370
820
  (e) => e.isDirectory()
371
821
  )) {
372
822
  if (ig.ignores(entry.name + "/")) continue;
373
- scanSkillDirs(join2(cwd, entry.name), cwd, ig, results, 1);
823
+ scanSkillDirs(join5(cwd, entry.name), cwd, ig, results, 1);
374
824
  }
375
825
  } catch {
376
826
  }
@@ -378,8 +828,8 @@ function scanLocal(cwd) {
378
828
  }
379
829
  function scanSkillDirs(dir, cwd, ig, results, depth) {
380
830
  if (depth > 3) return;
381
- const skillMd = join2(dir, "SKILL.md");
382
- if (existsSync2(skillMd)) {
831
+ const skillMd = join5(dir, "SKILL.md");
832
+ if (existsSync5(skillMd)) {
383
833
  const files = walkDir(dir, 1);
384
834
  for (const filePath of files) {
385
835
  const rel = relative(cwd, filePath);
@@ -399,11 +849,11 @@ function scanSkillDirs(dir, cwd, ig, results, depth) {
399
849
  return;
400
850
  }
401
851
  try {
402
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
852
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
403
853
  if (entry.isDirectory()) {
404
- const rel = relative(cwd, join2(dir, entry.name));
854
+ const rel = relative(cwd, join5(dir, entry.name));
405
855
  if (!ig.ignores(rel + "/")) {
406
- scanSkillDirs(join2(dir, entry.name), cwd, ig, results, depth + 1);
856
+ scanSkillDirs(join5(dir, entry.name), cwd, ig, results, depth + 1);
407
857
  }
408
858
  }
409
859
  }
@@ -411,17 +861,25 @@ function scanSkillDirs(dir, cwd, ig, results, depth) {
411
861
  }
412
862
  }
413
863
  function scanGlobal() {
414
- const home = homedir2();
864
+ const home = homedir5();
415
865
  const results = [];
416
866
  const globalPatterns = [
417
867
  { path: ".claude/CLAUDE.md", type: "rule", group: "claude-code" },
418
868
  { path: ".claude/settings.json", type: "config", group: "claude-code" },
419
- { path: ".cursor/mcp.json", type: "mcp", group: "cursor" },
420
869
  { path: ".continue/config.json", type: "config", group: "continue" },
421
- { path: ".aider.conf.yml", type: "config", group: "aider" }
870
+ { path: ".continue/config.yaml", type: "config", group: "continue" },
871
+ { path: ".aider.conf.yml", type: "config", group: "aider" },
872
+ { path: ".gemini/GEMINI.md", type: "rule", group: "gemini" },
873
+ { path: ".gemini/settings.json", type: "config", group: "gemini" },
874
+ { path: ".codex/config.toml", type: "config", group: "codex" },
875
+ {
876
+ path: ".codeium/windsurf/memories/global_rules.md",
877
+ type: "rule",
878
+ group: "windsurf"
879
+ }
422
880
  ];
423
881
  for (const pattern of globalPatterns) {
424
- const filePath = join2(home, pattern.path);
882
+ const filePath = join5(home, pattern.path);
425
883
  const content = readFileSafe(filePath);
426
884
  if (content !== null) {
427
885
  results.push({
@@ -441,7 +899,7 @@ function scanGlobal() {
441
899
  { dir: ".cursor/rules", type: "rule", group: "cursor" }
442
900
  ];
443
901
  for (const { dir, type, group } of globalDirs) {
444
- const dirPath = join2(home, dir);
902
+ const dirPath = join5(home, dir);
445
903
  const files = walkDir(dirPath, 2);
446
904
  for (const filePath of files) {
447
905
  const content = readFileSafe(filePath);
@@ -457,153 +915,166 @@ function scanGlobal() {
457
915
  }
458
916
  }
459
917
  }
918
+ const skillsRoot = join5(home, ".claude", "skills");
919
+ try {
920
+ for (const entry of readdirSync2(skillsRoot, { withFileTypes: true })) {
921
+ if (!entry.isDirectory()) continue;
922
+ const skillDir = join5(skillsRoot, entry.name);
923
+ if (!existsSync5(join5(skillDir, "SKILL.md"))) continue;
924
+ for (const filePath of walkDir(skillDir, 2)) {
925
+ const content = readFileSafe(filePath);
926
+ if (content !== null) {
927
+ results.push({
928
+ path: filePath,
929
+ relativePath: `~/${relative(home, filePath)}`,
930
+ content,
931
+ type: "skill",
932
+ source: "global",
933
+ group: "claude-code"
934
+ });
935
+ }
936
+ }
937
+ }
938
+ } catch {
939
+ }
460
940
  return results;
461
941
  }
462
942
 
463
- // src/classifier.ts
464
- import { basename, dirname } from "path";
465
-
466
- // src/stableKey.ts
467
- function computeStableKey(group, type, relPath) {
468
- return `${group}:${type}:${relPath}`;
469
- }
470
-
471
- // src/classifier.ts
472
- function classify(files) {
473
- const groups = /* @__PURE__ */ new Map();
474
- const singletons = [];
475
- const singletonRoots = /* @__PURE__ */ new Set([
476
- ".",
477
- "~",
478
- "~/.claude",
479
- "~/.cursor",
480
- "~/.continue",
481
- ".claude",
482
- ".cursor",
483
- ".github"
484
- ]);
485
- for (const file of files) {
486
- const dir = dirname(file.relativePath);
487
- const isSingleton = singletonRoots.has(dir);
488
- if (isSingleton) {
489
- singletons.push(file);
490
- } else {
491
- const key = `${file.group}:${file.source}:${file.type}:${dir}`;
492
- const existing = groups.get(key) ?? [];
493
- existing.push(file);
494
- groups.set(key, existing);
495
- }
496
- }
497
- const items = [];
498
- const scope = (source) => source === "global" ? "global" : "project";
499
- for (const file of singletons) {
500
- const s = scope(file.source);
501
- const relPath = file.relativePath.replace(/^~\/\.[^/]+\//, "").replace(/^\.[^/]+\//, "");
502
- items.push({
503
- type: file.type,
504
- name: file.relativePath,
505
- group: file.group,
506
- scope: s,
507
- stableKey: computeStableKey(file.group, file.type, relPath),
508
- files: [
509
- {
510
- name: basename(file.relativePath),
511
- content: file.content,
512
- path: file.relativePath
513
- }
514
- ]
515
- });
516
- }
517
- for (const [, groupFiles] of groups) {
518
- const first = groupFiles[0];
519
- const dir = dirname(first.relativePath);
520
- const s = scope(first.source);
521
- const relPath = dir.replace(/^~\/\.claude\//, "").replace(/^\.claude\//, "").replace(/^~\/\.cursor\//, "").replace(/^\.cursor\//, "");
522
- const typeLabel = first.type === "subagent" ? "subagents" : `${first.type}s`;
523
- items.push({
524
- type: first.type,
525
- name: dir,
526
- description: `${groupFiles.length} ${typeLabel}`,
527
- group: first.group,
528
- scope: s,
529
- stableKey: computeStableKey(first.group, first.type, relPath),
530
- files: groupFiles.map((f) => ({
531
- name: basename(f.relativePath),
532
- content: f.content,
533
- path: f.relativePath
534
- }))
535
- });
943
+ // src/theme.ts
944
+ import * as p from "@clack/prompts";
945
+ var esc = (code) => `\x1B[${code}m`;
946
+ var reset = esc("0");
947
+ var LIME = "163;230;53";
948
+ var BLACK = "0;0;0";
949
+ var YELLOW = "250;204;21";
950
+ var RED = "248;113;113";
951
+ var MUTED = "120;120;120";
952
+ var lime = (s) => `${esc(`38;2;${LIME}`)}${s}${reset}`;
953
+ var limeBold = (s) => `${esc("1")}${esc(`38;2;${LIME}`)}${s}${reset}`;
954
+ var bgLime = (s) => `${esc(`48;2;${LIME}`)}${esc(`38;2;${BLACK}`)}${s}${reset}`;
955
+ var yellow = (s) => `${esc(`38;2;${YELLOW}`)}${s}${reset}`;
956
+ var red = (s) => `${esc(`38;2;${RED}`)}${s}${reset}`;
957
+ var dim = (s) => `${esc(`38;2;${MUTED}`)}${s}${reset}`;
958
+ var bold = (s) => `${esc("1")}${s}${reset}`;
959
+ var banner = (cmd) => `${lime("\u25A0")} ${bgLime(` AISTACK `)} ${bold(cmd.toUpperCase())}`;
960
+ var BAR = `${esc(`38;2;${MUTED}`)}\u2502${reset}`;
961
+ function lines(items) {
962
+ for (const item of items) {
963
+ console.log(`${BAR} ${item}`);
536
964
  }
537
- return items;
965
+ }
966
+ function section(label, count) {
967
+ console.log(`${BAR}`);
968
+ const countStr = count !== void 0 ? ` ${dim(String(count))}` : "";
969
+ console.log(`${BAR} ${bold(label.toUpperCase())}${countStr}`);
970
+ }
971
+ function divider() {
972
+ console.log(`${BAR} ${dim("\u2500".repeat(40))}`);
973
+ }
974
+ function intro2(cmd) {
975
+ console.log();
976
+ p.intro(banner(cmd));
977
+ }
978
+ function outro2(msg) {
979
+ p.outro(msg);
980
+ console.log();
981
+ }
982
+ function outroError(msg) {
983
+ p.outro(red(msg));
984
+ console.log();
985
+ }
986
+ function outroCancel(msg = "cancelled") {
987
+ p.cancel(dim(msg));
988
+ console.log();
989
+ }
990
+ function outroSkipped(msg) {
991
+ p.outro(dim(msg));
992
+ console.log();
538
993
  }
539
994
 
540
995
  // src/commands/collect.ts
996
+ var REPO_LINK_KEY = "\0repo-link";
541
997
  async function collectCommand(options) {
542
998
  intro2("collect");
543
999
  const token = getToken();
544
1000
  if (!token) {
545
- p3.log.error(`Not authenticated. Run ${limeBold("aistack login")} first.`);
1001
+ p2.log.error(
1002
+ `Not authenticated. Run ${limeBold("npx @use-aistack/cli login")} first.`
1003
+ );
546
1004
  outroError("not authenticated");
547
1005
  process.exit(1);
548
1006
  }
549
1007
  const cwd = process.cwd();
550
- const savedName = getProjectName(cwd);
551
1008
  const savedExcluded = getExcludedPaths(cwd);
552
- const s = p3.spinner();
1009
+ const s = p2.spinner();
553
1010
  s.start("Scanning...");
554
1011
  const localFiles = scanLocal(cwd);
555
1012
  const globalFiles = options.global ? scanGlobal() : [];
556
1013
  s.stop("Scan complete");
557
1014
  if (localFiles.length === 0 && globalFiles.length === 0) {
558
- p3.log.warn("No AI configuration files found.");
1015
+ p2.log.warn("No AI configuration files found.");
559
1016
  outroSkipped("nothing to collect");
560
1017
  return;
561
1018
  }
562
- let projectName;
563
- if (savedName) {
564
- p3.log.info(`${dim("PROJECT")} ${limeBold(savedName)}`);
565
- projectName = savedName;
566
- } else {
567
- const defaultName = basename2(cwd);
568
- const name = await p3.text({
569
- message: "Project name:",
570
- defaultValue: defaultName,
571
- placeholder: defaultName
572
- });
573
- if (p3.isCancel(name)) {
574
- outroCancel();
575
- process.exit(0);
576
- }
577
- projectName = name || defaultName;
578
- }
579
1019
  const allFiles = [...localFiles, ...globalFiles];
580
1020
  let selectedFiles = allFiles.filter(
581
1021
  (f) => !savedExcluded.includes(f.relativePath)
582
1022
  );
583
1023
  let excluded = allFiles.filter((f) => savedExcluded.includes(f.relativePath));
584
- p3.log.info(
1024
+ p2.log.info(
585
1025
  `${lime(String(selectedFiles.length))} included${excluded.length > 0 ? ` \xB7 ${dim(String(excluded.length) + " excluded")}` : ""}`
586
1026
  );
587
- let allInstructions = classify(selectedFiles);
588
- let existingProject = null;
1027
+ const detectedLinks = [];
1028
+ const repoUrl = detectRepoUrl(cwd);
1029
+ if (repoUrl) {
1030
+ detectedLinks.push({
1031
+ key: REPO_LINK_KEY,
1032
+ resource: buildRepoLinkResource(repoUrl),
1033
+ label: `repo \xB7 ${repoNameFromCanonical(repoUrl)}`
1034
+ });
1035
+ }
1036
+ for (const resource of detectInstalledPlugins()) {
1037
+ detectedLinks.push({
1038
+ key: `\0plugin:${resource.stableKey}`,
1039
+ resource,
1040
+ label: `plugin \xB7 ${resource.name}`
1041
+ });
1042
+ }
1043
+ for (const resource of detectMcpServers(cwd)) {
1044
+ detectedLinks.push({
1045
+ key: `\0mcp:${resource.stableKey}`,
1046
+ resource,
1047
+ label: `mcp \xB7 ${resource.name}`
1048
+ });
1049
+ }
1050
+ for (const resource of detectHooks(cwd)) {
1051
+ detectedLinks.push({
1052
+ key: `\0hook:${resource.stableKey}`,
1053
+ resource,
1054
+ label: `hook \xB7 ${resource.name}`
1055
+ });
1056
+ }
1057
+ const includedLinks = new Set(
1058
+ detectedLinks.filter((l) => !savedExcluded.includes(l.key)).map((l) => l.key)
1059
+ );
1060
+ const withLinks = (base) => [
1061
+ ...base,
1062
+ ...detectedLinks.filter((l) => includedLinks.has(l.key)).map((l) => l.resource)
1063
+ ];
1064
+ let allResources = withLinks(classify(selectedFiles));
1065
+ let existingStack = null;
589
1066
  try {
590
- const check = await projectsCheck(token, projectName);
591
- if (check.exists && check.slug) {
592
- const shortId = check.slug.includes("-") ? check.slug.slice(check.slug.lastIndexOf("-") + 1) : check.slug;
593
- existingProject = await projectGet(shortId);
594
- }
1067
+ existingStack = await stackGet(token);
595
1068
  } catch (err) {
596
- p3.log.error(err instanceof Error ? err.message : String(err));
1069
+ p2.log.error(err instanceof Error ? err.message : String(err));
597
1070
  outroError("error");
598
1071
  process.exit(1);
599
1072
  }
600
- if (existingProject) {
601
- const diff = diffInstructions(
602
- allInstructions,
603
- existingProject.instructions
604
- );
605
- if (diff.changed === 0 && diff.added === 0 && diff.removed === 0) {
606
- p3.log.info("No changes since last collect.");
1073
+ if (existingStack) {
1074
+ const diff = diffResources(allResources, existingStack.resources);
1075
+ const changeCount = diff.added + diff.changed + diff.removed;
1076
+ if (changeCount === 0) {
1077
+ p2.log.info("No changes since last collect.");
607
1078
  outroSkipped("nothing to upload");
608
1079
  return;
609
1080
  }
@@ -624,7 +1095,7 @@ async function collectCommand(options) {
624
1095
  const local = selectedFiles.filter((f) => f.source === "local");
625
1096
  const global = selectedFiles.filter((f) => f.source === "global");
626
1097
  if (local.length > 0) {
627
- p3.log.step(`${bold("LOCAL")} ${dim(String(local.length))}`);
1098
+ p2.log.step(`${bold("LOCAL")} ${dim(String(local.length))}`);
628
1099
  divider();
629
1100
  for (const [type, files] of groupByType(local)) {
630
1101
  lines([`${lime(type.toUpperCase())} ${dim(`${files.length}`)}`]);
@@ -633,7 +1104,7 @@ async function collectCommand(options) {
633
1104
  divider();
634
1105
  }
635
1106
  if (global.length > 0) {
636
- p3.log.step(`${bold("GLOBAL")} ${dim(String(global.length))}`);
1107
+ p2.log.step(`${bold("GLOBAL")} ${dim(String(global.length))}`);
637
1108
  divider();
638
1109
  for (const [type, files] of groupByType(global)) {
639
1110
  lines([`${lime(type.toUpperCase())} ${dim(`${files.length}`)}`]);
@@ -641,60 +1112,79 @@ async function collectCommand(options) {
641
1112
  }
642
1113
  divider();
643
1114
  }
1115
+ const shownLinks = detectedLinks.filter((l) => includedLinks.has(l.key));
1116
+ if (shownLinks.length > 0) {
1117
+ p2.log.step(`${bold("LINKS")} ${dim(String(shownLinks.length))}`);
1118
+ divider();
1119
+ lines(shownLinks.map((l) => dim(` ${l.label}`)));
1120
+ divider();
1121
+ }
644
1122
  }
645
- const action = await p3.select({
646
- message: existingProject ? "Upload changes?" : `Upload ${bold(String(selectedFiles.length))} files as ${limeBold(projectName)}?`,
1123
+ const action = await p2.select({
1124
+ message: existingStack ? "Upload changes?" : `Upload ${bold(String(selectedFiles.length))} files to your stack?`,
647
1125
  options: [
648
1126
  { value: "upload", label: "Upload" },
649
1127
  { value: "customize", label: "Select files" },
650
1128
  { value: "cancel", label: "Cancel" }
651
1129
  ]
652
1130
  });
653
- if (p3.isCancel(action) || action === "cancel") {
1131
+ if (p2.isCancel(action) || action === "cancel") {
654
1132
  outroCancel();
655
1133
  process.exit(0);
656
1134
  }
657
1135
  if (action === "customize") {
658
- const selected = await p3.multiselect({
1136
+ const linkOptions = detectedLinks.map((l) => ({
1137
+ value: l.key,
1138
+ label: l.label,
1139
+ hint: "link"
1140
+ }));
1141
+ const selected = await p2.multiselect({
659
1142
  message: "Select files to include:",
660
- options: allFiles.map((f) => ({
661
- value: f.relativePath,
662
- label: f.relativePath,
663
- hint: `${f.type}${f.source === "global" ? " \xB7 global" : ""}`
664
- })),
665
- initialValues: selectedFiles.map((f) => f.relativePath)
1143
+ options: [
1144
+ ...linkOptions,
1145
+ ...allFiles.map((f) => ({
1146
+ value: f.relativePath,
1147
+ label: f.relativePath,
1148
+ hint: `${f.type}${f.source === "global" ? " \xB7 global" : ""}`
1149
+ }))
1150
+ ],
1151
+ initialValues: [
1152
+ ...detectedLinks.filter((l) => includedLinks.has(l.key)).map((l) => l.key),
1153
+ ...selectedFiles.map((f) => f.relativePath)
1154
+ ]
666
1155
  });
667
- if (p3.isCancel(selected)) {
1156
+ if (p2.isCancel(selected)) {
668
1157
  outroCancel();
669
1158
  process.exit(0);
670
1159
  }
671
1160
  const selectedSet = new Set(selected);
672
1161
  selectedFiles = allFiles.filter((f) => selectedSet.has(f.relativePath));
673
1162
  excluded = allFiles.filter((f) => !selectedSet.has(f.relativePath));
674
- allInstructions = classify(selectedFiles);
675
- if (selectedFiles.length === 0) {
676
- p3.log.warn("No files selected.");
1163
+ includedLinks.clear();
1164
+ for (const l of detectedLinks) {
1165
+ if (selectedSet.has(l.key)) includedLinks.add(l.key);
1166
+ }
1167
+ allResources = withLinks(classify(selectedFiles));
1168
+ if (selectedFiles.length === 0 && includedLinks.size === 0) {
1169
+ p2.log.warn("No files selected.");
677
1170
  outroSkipped("nothing to collect");
678
1171
  process.exit(0);
679
1172
  }
680
1173
  }
681
1174
  s.start("Uploading...");
682
1175
  try {
683
- const result = await projectsCollect(token, {
684
- name: projectName,
685
- instructions: allInstructions
686
- });
1176
+ const result = await stackCollect(token, { resources: allResources });
687
1177
  s.stop(lime("Uploaded"));
688
- saveProjectSettings(
689
- cwd,
690
- projectName,
691
- excluded.map((f) => f.relativePath)
692
- );
693
- p3.log.success(dim(result.url));
1178
+ const excludedKeys = excluded.map((f) => f.relativePath);
1179
+ for (const l of detectedLinks) {
1180
+ if (!includedLinks.has(l.key)) excludedKeys.push(l.key);
1181
+ }
1182
+ saveExcludedPaths(cwd, excludedKeys);
1183
+ p2.log.success(dim(result.url));
694
1184
  outro2(lime("done"));
695
1185
  } catch (err) {
696
1186
  s.stop("Upload failed");
697
- p3.log.error(err instanceof Error ? err.message : String(err));
1187
+ p2.log.error(err instanceof Error ? err.message : String(err));
698
1188
  outroError("upload failed");
699
1189
  process.exit(1);
700
1190
  }
@@ -727,16 +1217,16 @@ function groupByType(files) {
727
1217
  }
728
1218
  return sorted;
729
1219
  }
730
- function diffInstructions(current, existing) {
1220
+ function diffResources(current, existing) {
731
1221
  const existingMap = /* @__PURE__ */ new Map();
732
1222
  for (const item of existing) {
733
- for (const file of item.files) {
1223
+ for (const file of item.files ?? []) {
734
1224
  existingMap.set(file.path ?? file.name, file.content);
735
1225
  }
736
1226
  }
737
1227
  const currentMap = /* @__PURE__ */ new Map();
738
1228
  for (const item of current) {
739
- for (const file of item.files) {
1229
+ for (const file of item.files ?? []) {
740
1230
  currentMap.set(file.path ?? file.name, file.content);
741
1231
  }
742
1232
  }
@@ -763,64 +1253,246 @@ function diffInstructions(current, existing) {
763
1253
  details.push({ name: key, status: "removed" });
764
1254
  }
765
1255
  }
766
- return { added, changed, removed, unchanged, details };
767
- }
768
-
769
- // src/commands/create.ts
770
- import * as p4 from "@clack/prompts";
771
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
772
- import { dirname as dirname2, join as join3 } from "path";
773
- async function createCommand(slugOrShortId) {
774
- intro2("create");
775
- const s = p4.spinner();
776
- s.start("Fetching project...");
777
- const shortId = slugOrShortId.includes("-") ? slugOrShortId.slice(slugOrShortId.lastIndexOf("-") + 1) : slugOrShortId;
778
- let project;
779
- try {
780
- project = await projectGet(shortId);
781
- if (!project) {
782
- s.stop("Not found");
783
- p4.log.error(`Project "${slugOrShortId}" not found.`);
784
- outroError("not found");
785
- process.exit(1);
786
- }
787
- s.stop(bold(project.name));
788
- } catch (err) {
789
- s.stop("Failed to fetch project");
790
- p4.log.error(err instanceof Error ? err.message : String(err));
791
- outroError("error");
792
- process.exit(1);
793
- }
794
- const localFiles = [];
795
- const globalFiles = [];
796
- for (const item of project.instructions) {
797
- const isGlobal = item.scope === "global";
798
- for (const file of item.files) {
799
- const writePath = file.path ?? file.name;
800
- if (isGlobal) {
801
- globalFiles.push({ path: writePath, content: file.content });
802
- } else {
803
- localFiles.push({ path: writePath, content: file.content });
1256
+ const linkLabel = (item) => {
1257
+ if (item.upstream)
1258
+ return `link: ${repoNameFromCanonical(item.upstream.repoUrl)}`;
1259
+ if (item.pkg) return `link: ${item.pkg.id}`;
1260
+ return `link: ${item.name}`;
1261
+ };
1262
+ const linkMap = (items) => {
1263
+ const map = /* @__PURE__ */ new Map();
1264
+ for (const item of items) {
1265
+ if ((item.upstream || item.pkg) && !item.files?.length) {
1266
+ map.set(item.stableKey, item);
804
1267
  }
805
1268
  }
1269
+ return map;
1270
+ };
1271
+ const existingLinks = linkMap(existing);
1272
+ const currentLinks = linkMap(current);
1273
+ for (const [key, item] of currentLinks) {
1274
+ if (!existingLinks.has(key)) {
1275
+ added++;
1276
+ details.push({ name: linkLabel(item), status: "added" });
1277
+ }
806
1278
  }
807
- if (globalFiles.length > 0) {
808
- section("global config", globalFiles.length);
809
- lines([dim("view only")]);
810
- lines(globalFiles.map((f) => dim(f.path)));
1279
+ for (const [key, item] of existingLinks) {
1280
+ if (!currentLinks.has(key)) {
1281
+ removed++;
1282
+ details.push({ name: linkLabel(item), status: "removed" });
1283
+ }
811
1284
  }
812
- if (localFiles.length === 0) {
813
- p4.log.warn("No local files to write.");
814
- outroSkipped("nothing to create");
815
- return;
1285
+ return { added, changed, removed, unchanged, details };
1286
+ }
1287
+
1288
+ // src/commands/connect.ts
1289
+ import { spawnSync } from "child_process";
1290
+ import { cpSync, existsSync as existsSync6 } from "fs";
1291
+ import { homedir as homedir6 } from "os";
1292
+ import { dirname as dirname2, join as join6 } from "path";
1293
+ import { fileURLToPath } from "url";
1294
+ import * as p3 from "@clack/prompts";
1295
+ var MANUAL_MCP_ADD = "claude mcp add --scope user aistack -- npx -y @use-aistack/cli mcp";
1296
+ var MCP_ADD_ARGS = [
1297
+ "mcp",
1298
+ "add",
1299
+ "--scope",
1300
+ "user",
1301
+ "aistack",
1302
+ "--",
1303
+ "npx",
1304
+ "-y",
1305
+ "@use-aistack/cli",
1306
+ "mcp"
1307
+ ];
1308
+ var MCP_REMOVE_ARGS = ["mcp", "remove", "--scope", "user", "aistack"];
1309
+ var SKILL_DEST = join6(homedir6(), ".claude", "skills", "aistack-sync");
1310
+ function runClaude(args) {
1311
+ const r = spawnSync("claude", args, { encoding: "utf-8" });
1312
+ const notFound = r.error !== void 0 && r.error.code === "ENOENT";
1313
+ return {
1314
+ notFound,
1315
+ status: r.status,
1316
+ output: `${r.stdout ?? ""}${r.stderr ?? ""}`
1317
+ };
1318
+ }
1319
+ function claudeOnPath(run = runClaude) {
1320
+ return !run(["--version"]).notFound;
1321
+ }
1322
+ function findSkillSource(fromDir = dirname2(fileURLToPath(import.meta.url))) {
1323
+ let dir = fromDir;
1324
+ for (let i = 0; i < 4; i++) {
1325
+ const candidate = join6(dir, "skills", "aistack-sync");
1326
+ if (existsSync6(join6(candidate, "SKILL.md"))) return candidate;
1327
+ const parent = dirname2(dir);
1328
+ if (parent === dir) break;
1329
+ dir = parent;
1330
+ }
1331
+ return null;
1332
+ }
1333
+ function installClaudeConnect(run = runClaude, copySkill = (src, dest) => cpSync(src, dest, { recursive: true })) {
1334
+ const source = findSkillSource();
1335
+ if (source === null) {
1336
+ return {
1337
+ ok: false,
1338
+ message: "this install is missing its bundled Skill (skills/aistack-sync) \u2014 nothing was installed"
1339
+ };
1340
+ }
1341
+ const add = run(MCP_ADD_ARGS);
1342
+ if (add.notFound) {
1343
+ return {
1344
+ ok: false,
1345
+ message: `claude was not found on PATH \u2014 nothing was installed. Manual install:
1346
+ ${MANUAL_MCP_ADD}`
1347
+ };
1348
+ }
1349
+ const alreadyRegistered = add.status !== 0 && add.output.includes("already exists");
1350
+ if (add.status !== 0 && !alreadyRegistered) {
1351
+ return {
1352
+ ok: false,
1353
+ message: `claude mcp add failed \u2014 nothing was installed.
1354
+ ${add.output.trim()}`
1355
+ };
1356
+ }
1357
+ try {
1358
+ copySkill(source, SKILL_DEST);
1359
+ } catch (e) {
1360
+ if (!alreadyRegistered) run(MCP_REMOVE_ARGS);
1361
+ return {
1362
+ ok: false,
1363
+ message: `copying the Skill to ${SKILL_DEST} failed \u2014 the MCP registration was ${alreadyRegistered ? "left as it was" : "rolled back"}.
1364
+ ${e instanceof Error ? e.message : String(e)}`
1365
+ };
1366
+ }
1367
+ return {
1368
+ ok: true,
1369
+ message: `Installed. Say ${limeBold('"sync my stack"')} in any Claude Code session.`
1370
+ };
1371
+ }
1372
+ async function connectCommand(harness) {
1373
+ intro2("connect");
1374
+ if (harness !== "claude") {
1375
+ outroError(`unknown harness "${harness}" \u2014 supported: claude`);
1376
+ process.exitCode = 1;
1377
+ return;
1378
+ }
1379
+ if (!claudeOnPath()) {
1380
+ p3.log.warn(
1381
+ `claude was not found on PATH. Manual install:
1382
+ ${dim(MANUAL_MCP_ADD)}
1383
+ plus copy skills/aistack-sync from this package to ${dim(SKILL_DEST)}`
1384
+ );
1385
+ outroSkipped("nothing was installed");
1386
+ return;
1387
+ }
1388
+ const result = installClaudeConnect();
1389
+ if (!result.ok) {
1390
+ outroError(result.message);
1391
+ process.exitCode = 1;
1392
+ return;
1393
+ }
1394
+ p3.log.success(result.message);
1395
+ outro2("done");
1396
+ }
1397
+ async function offerConnectUpsell() {
1398
+ if (getSettings().connectClaudeAnswered === true) return;
1399
+ if (!claudeOnPath()) return;
1400
+ const answer = await p3.select({
1401
+ message: "Sync from inside Claude Code too?",
1402
+ options: [
1403
+ {
1404
+ value: "later",
1405
+ label: "Not now",
1406
+ hint: "this question will not come back"
1407
+ },
1408
+ {
1409
+ value: "install",
1410
+ label: "Install",
1411
+ hint: "adds the aistack MCP server + Skill to Claude Code"
1412
+ }
1413
+ ],
1414
+ initialValue: "later"
1415
+ });
1416
+ if (p3.isCancel(answer)) return;
1417
+ saveSettings({ connectClaudeAnswered: true });
1418
+ if (answer === "install") {
1419
+ const result = installClaudeConnect();
1420
+ if (result.ok) {
1421
+ p3.log.success(result.message);
1422
+ } else {
1423
+ p3.log.error(result.message);
1424
+ }
1425
+ return;
1426
+ }
1427
+ p3.log.message(
1428
+ `If you change your mind: ${limeBold("npx @use-aistack/cli connect claude")}`
1429
+ );
1430
+ }
1431
+
1432
+ // src/commands/create.ts
1433
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1434
+ import { dirname as dirname3, join as join7 } from "path";
1435
+ import * as p4 from "@clack/prompts";
1436
+ async function createCommand() {
1437
+ intro2("create");
1438
+ const token = getToken();
1439
+ if (!token) {
1440
+ p4.log.error(
1441
+ `Not authenticated. Run ${limeBold("npx @use-aistack/cli login")} first.`
1442
+ );
1443
+ outroError("not authenticated");
1444
+ process.exit(1);
1445
+ }
1446
+ const s = p4.spinner();
1447
+ s.start("Fetching stack...");
1448
+ let stack;
1449
+ try {
1450
+ stack = await stackGet(token);
1451
+ if (!stack) {
1452
+ s.stop("Not found");
1453
+ p4.log.error("No stack found. Create a stack on aistack.to first.");
1454
+ outroError("not found");
1455
+ process.exit(1);
1456
+ }
1457
+ s.stop(bold(stack.name));
1458
+ } catch (err) {
1459
+ s.stop("Failed to fetch stack");
1460
+ p4.log.error(err instanceof Error ? err.message : String(err));
1461
+ outroError("error");
1462
+ process.exit(1);
1463
+ }
1464
+ const localFiles = [];
1465
+ for (const item of stack.resources) {
1466
+ for (const file of item.files ?? []) {
1467
+ localFiles.push({ path: file.path ?? file.name, content: file.content });
1468
+ }
1469
+ }
1470
+ const linked = stack.resources.filter(
1471
+ (item) => (item.upstream || item.pkg) && !item.files?.length
1472
+ );
1473
+ if (linked.length > 0) {
1474
+ section("linked", linked.length);
1475
+ lines([dim("view only")]);
1476
+ lines(
1477
+ linked.map(
1478
+ (item) => dim(
1479
+ item.upstream?.repoUrl ?? (item.pkg ? `${item.pkg.registry}:${item.pkg.id}` : "")
1480
+ )
1481
+ )
1482
+ );
1483
+ }
1484
+ if (localFiles.length === 0) {
1485
+ p4.log.warn("No local files to write.");
1486
+ outroSkipped("nothing to create");
1487
+ return;
816
1488
  }
817
1489
  const cwd = process.cwd();
818
1490
  const toWrite = [];
819
1491
  const skipped = [];
820
1492
  for (const f of localFiles) {
821
- const fullPath = join3(cwd, f.path);
822
- if (existsSync3(fullPath)) {
823
- const existing = readFileSync3(fullPath, "utf-8");
1493
+ const fullPath = join7(cwd, f.path);
1494
+ if (existsSync7(fullPath)) {
1495
+ const existing = readFileSync6(fullPath, "utf-8");
824
1496
  skipped.push({ path: f.path, differs: existing !== f.content });
825
1497
  } else {
826
1498
  toWrite.push(f);
@@ -848,8 +1520,8 @@ async function createCommand(slugOrShortId) {
848
1520
  process.exit(0);
849
1521
  }
850
1522
  for (const f of toWrite) {
851
- const fullPath = join3(cwd, f.path);
852
- const dir = dirname2(fullPath);
1523
+ const fullPath = join7(cwd, f.path);
1524
+ const dir = dirname3(fullPath);
853
1525
  mkdirSync2(dir, { recursive: true });
854
1526
  writeFileSync2(fullPath, f.content);
855
1527
  }
@@ -859,11 +1531,1694 @@ async function createCommand(slugOrShortId) {
859
1531
  outro2(lime("done"));
860
1532
  }
861
1533
 
1534
+ // src/commands/login.ts
1535
+ import { hostname } from "os";
1536
+ import * as p5 from "@clack/prompts";
1537
+ import open from "open";
1538
+ function proposedMachineName(read = hostname) {
1539
+ try {
1540
+ const name = read().trim().replace(/\.local$/i, "");
1541
+ if (!name || name.length > 64) return void 0;
1542
+ return name;
1543
+ } catch {
1544
+ return void 0;
1545
+ }
1546
+ }
1547
+ async function loginCommand() {
1548
+ intro2("login");
1549
+ const s = p5.spinner();
1550
+ s.start("Starting authentication...");
1551
+ let session;
1552
+ try {
1553
+ session = await authStart(proposedMachineName());
1554
+ s.stop("Session created");
1555
+ } catch (err) {
1556
+ s.stop("Failed to start authentication");
1557
+ p5.log.error(err instanceof Error ? err.message : String(err));
1558
+ outroError("error");
1559
+ process.exit(1);
1560
+ }
1561
+ p5.log.info(`${dim("CODE")} ${limeBold(session.userCode)}`);
1562
+ p5.log.info(`${dim("OPEN")} ${dim(session.authUrl)}`);
1563
+ try {
1564
+ await open(session.authUrl);
1565
+ } catch {
1566
+ p5.log.warn(
1567
+ "Could not open browser automatically. Please visit the URL above."
1568
+ );
1569
+ }
1570
+ s.start("Waiting for approval...");
1571
+ const maxAttempts = 36;
1572
+ for (let i = 0; i < maxAttempts; i++) {
1573
+ await new Promise((resolve) => setTimeout(resolve, 5e3));
1574
+ try {
1575
+ const result = await authPoll(session.secretId);
1576
+ if (result.status === "approved" && result.token) {
1577
+ s.stop(lime("Authenticated"));
1578
+ saveToken(result.token, result.userId);
1579
+ p5.log.success(
1580
+ `Token saved. Run ${limeBold("npx @use-aistack/cli collect")} to get started.`
1581
+ );
1582
+ outro2(lime("done"));
1583
+ return;
1584
+ }
1585
+ if (result.status === "expired") {
1586
+ s.stop("Session expired");
1587
+ p5.log.error("Authentication session expired. Please try again.");
1588
+ outroError("expired");
1589
+ process.exit(1);
1590
+ }
1591
+ } catch (err) {
1592
+ s.stop("Error polling");
1593
+ p5.log.error(err instanceof Error ? err.message : String(err));
1594
+ outroError("error");
1595
+ process.exit(1);
1596
+ }
1597
+ }
1598
+ s.stop("Timed out");
1599
+ p5.log.error("Authentication timed out after 3 minutes. Please try again.");
1600
+ outroError("timed out");
1601
+ process.exit(1);
1602
+ }
1603
+
1604
+ // src/commands/sync.ts
1605
+ import * as p6 from "@clack/prompts";
1606
+
1607
+ // src/sync/stage.ts
1608
+ import { createHash } from "crypto";
1609
+
1610
+ // src/transcripts/pricing.ts
1611
+ var PRICING_TABLE_VERSION = "anthropic-list-2026-07-25";
1612
+ var CACHE_WRITE_5M_MULTIPLIER = 1.25;
1613
+ var CACHE_WRITE_1H_MULTIPLIER = 2;
1614
+ var CACHE_READ_MULTIPLIER = 0.1;
1615
+ var SONNET_5_INTRO_ENDS_MS = Date.UTC(2026, 8, 1);
1616
+ var PRICES = {
1617
+ "claude-fable-5": [{ from: null, to: null, input: 10, output: 50 }],
1618
+ "claude-mythos-5": [{ from: null, to: null, input: 10, output: 50 }],
1619
+ "claude-opus-5": [{ from: null, to: null, input: 5, output: 25 }],
1620
+ "claude-opus-4-8": [{ from: null, to: null, input: 5, output: 25 }],
1621
+ "claude-opus-4-7": [{ from: null, to: null, input: 5, output: 25 }],
1622
+ "claude-opus-4-6": [{ from: null, to: null, input: 5, output: 25 }],
1623
+ "claude-sonnet-5": [
1624
+ { from: null, to: SONNET_5_INTRO_ENDS_MS, input: 2, output: 10 },
1625
+ { from: SONNET_5_INTRO_ENDS_MS, to: null, input: 3, output: 15 }
1626
+ ],
1627
+ "claude-sonnet-4-6": [{ from: null, to: null, input: 3, output: 15 }],
1628
+ "claude-haiku-4-5": [{ from: null, to: null, input: 1, output: 5 }],
1629
+ // Fast mode (research preview) — Claude API only, Opus 5 / Opus 4.8 only.
1630
+ // Opus 4.7 fast mode was removed, so there is deliberately no 4-7 entry.
1631
+ "claude-opus-5#fast": [{ from: null, to: null, input: 10, output: 50 }],
1632
+ "claude-opus-4-8#fast": [{ from: null, to: null, input: 10, output: 50 }]
1633
+ };
1634
+ function normalizeModel(model) {
1635
+ const [base, suffix] = model.split("#");
1636
+ const stripped = base.replace(/-\d{8}$/, "");
1637
+ return suffix ? `${stripped}#${suffix}` : stripped;
1638
+ }
1639
+ function baseModelId(modelKey) {
1640
+ return modelKey.split("#")[0];
1641
+ }
1642
+ function priceAt(modelKey, atMs) {
1643
+ if (atMs === null) return null;
1644
+ const periods = PRICES[modelKey];
1645
+ if (!periods) return null;
1646
+ for (const p7 of periods) {
1647
+ if ((p7.from === null || atMs >= p7.from) && (p7.to === null || atMs < p7.to)) {
1648
+ return p7;
1649
+ }
1650
+ }
1651
+ return null;
1652
+ }
1653
+ function isPricedModel(modelKey) {
1654
+ return PRICES[modelKey] !== void 0;
1655
+ }
1656
+ function apiEquivalentCost(modelKey, t, atMs) {
1657
+ const p7 = priceAt(modelKey, atMs);
1658
+ if (!p7) return null;
1659
+ const M = 1e6;
1660
+ return (t.input * p7.input + t.output * p7.output + (t.cacheWrite5m + t.cacheWriteUnsplit) * p7.input * CACHE_WRITE_5M_MULTIPLIER + t.cacheWrite1h * p7.input * CACHE_WRITE_1H_MULTIPLIER + t.cacheRead * p7.input * CACHE_READ_MULTIPLIER) / M;
1661
+ }
1662
+
1663
+ // src/transcripts/analyzer.ts
1664
+ var asObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
1665
+ var asStr = (v) => typeof v === "string" && v.length > 0 ? v : null;
1666
+ var asNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
1667
+ var asArr = (v) => Array.isArray(v) ? v : [];
1668
+ var NAME_UNSAFE_RE = (
1669
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping them is the point
1670
+ /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/g
1671
+ );
1672
+ var NAME_MAX = 64;
1673
+ function cleanName(s) {
1674
+ const stripped = s.replace(NAME_UNSAFE_RE, "\uFFFD").trim();
1675
+ if (stripped.length === 0) return "(unnamed)";
1676
+ return stripped.length > NAME_MAX ? `${stripped.slice(0, NAME_MAX - 1)}\u2026` : stripped;
1677
+ }
1678
+ function isDisplaySafeName(s) {
1679
+ if (s.length === 0 || s.trim().length === 0) return false;
1680
+ if (s.length > NAME_MAX) return false;
1681
+ return !new RegExp(NAME_UNSAFE_RE.source).test(s);
1682
+ }
1683
+ var asName = (v) => {
1684
+ const s = asStr(v);
1685
+ return s === null ? null : cleanName(s);
1686
+ };
1687
+ function createAggregate() {
1688
+ return {
1689
+ files: 0,
1690
+ lines: 0,
1691
+ parseErrors: 0,
1692
+ records: 0,
1693
+ assistantRecords: 0,
1694
+ distinctResponses: 0,
1695
+ continuationsFolded: 0,
1696
+ realReplaysFolded: 0,
1697
+ supersededByLarger: 0,
1698
+ unkeyedResponses: 0,
1699
+ syntheticRecords: 0,
1700
+ syntheticTokens: 0,
1701
+ toolBlocksWithoutId: 0,
1702
+ fallbackAttempts: 0,
1703
+ untypedMirrors: 0,
1704
+ untimestampedResponses: 0,
1705
+ projectDirs: /* @__PURE__ */ new Set(),
1706
+ ccVersions: /* @__PURE__ */ new Set(),
1707
+ mirroredIterationTypes: /* @__PURE__ */ new Map(),
1708
+ byModel: /* @__PURE__ */ new Map(),
1709
+ sidechainTokens: 0,
1710
+ mainTokens: 0,
1711
+ sessions: /* @__PURE__ */ new Set(),
1712
+ activeDays: /* @__PURE__ */ new Set(),
1713
+ firstTs: null,
1714
+ lastTs: null,
1715
+ toolCalls: /* @__PURE__ */ new Map(),
1716
+ skillCalls: /* @__PURE__ */ new Map(),
1717
+ mcpServerCalls: /* @__PURE__ */ new Map(),
1718
+ mcpToolCalls: /* @__PURE__ */ new Map(),
1719
+ subagentCalls: /* @__PURE__ */ new Map(),
1720
+ slashCommands: /* @__PURE__ */ new Map(),
1721
+ toolCallDedup: /* @__PURE__ */ new Set(),
1722
+ thinkingBlocks: 0,
1723
+ textBlocks: 0,
1724
+ webSearchRequests: 0,
1725
+ webFetchRequests: 0,
1726
+ seen: /* @__PURE__ */ new Map()
1727
+ };
1728
+ }
1729
+ var bump = (m, k, n = 1) => m.set(k, (m.get(k) ?? 0) + n);
1730
+ function emptyUsage() {
1731
+ return {
1732
+ input: 0,
1733
+ output: 0,
1734
+ cacheWrite5m: 0,
1735
+ cacheWrite1h: 0,
1736
+ cacheWriteUnsplit: 0,
1737
+ cacheRead: 0,
1738
+ messages: 0,
1739
+ costUSD: 0,
1740
+ unpricedTokens: 0
1741
+ };
1742
+ }
1743
+ var countsTotal = (t) => t.input + t.output + t.cacheWrite5m + t.cacheWrite1h + t.cacheWriteUnsplit + t.cacheRead;
1744
+ function ingestRecord(agg, raw, ctx) {
1745
+ const rec = asObj(raw);
1746
+ if (!rec) return;
1747
+ agg.records++;
1748
+ agg.projectDirs.add(ctx.projectDir);
1749
+ const version = asStr(rec.version);
1750
+ if (version) agg.ccVersions.add(cleanName(version));
1751
+ const sessionId = asStr(rec.sessionId);
1752
+ if (sessionId) agg.sessions.add(sessionId);
1753
+ let tsMs = null;
1754
+ const timestamp = asStr(rec.timestamp);
1755
+ if (timestamp) {
1756
+ const ts = Date.parse(timestamp);
1757
+ if (!Number.isNaN(ts)) {
1758
+ tsMs = ts;
1759
+ agg.activeDays.add(timestamp.slice(0, 10));
1760
+ agg.firstTs = agg.firstTs === null ? ts : Math.min(agg.firstTs, ts);
1761
+ agg.lastTs = agg.lastTs === null ? ts : Math.max(agg.lastTs, ts);
1762
+ }
1763
+ }
1764
+ const type = asStr(rec.type);
1765
+ if (type === "assistant") ingestAssistant(agg, rec, tsMs);
1766
+ else if (type === "user") ingestUser(agg, rec);
1767
+ }
1768
+ function ingestAssistant(agg, rec, tsMs) {
1769
+ agg.assistantRecords++;
1770
+ const msg = asObj(rec.message);
1771
+ if (!msg) return;
1772
+ const messageId = asStr(msg.id);
1773
+ const requestId = asStr(rec.requestId);
1774
+ const existing = messageId === null ? void 0 : agg.seen.get(messageId);
1775
+ const isReplay = existing !== void 0 && existing.requestId !== requestId;
1776
+ if (!isReplay) ingestContentBlocks(agg, msg.content);
1777
+ const usage = asObj(msg.usage);
1778
+ if (!usage) return;
1779
+ const model = asName(msg.model) ?? "(unknown)";
1780
+ if (model.startsWith("<")) {
1781
+ agg.syntheticRecords++;
1782
+ agg.syntheticTokens += countsTotal(readCounts(usage));
1783
+ return;
1784
+ }
1785
+ if (tsMs === null) agg.untimestampedResponses++;
1786
+ const sidechain = rec.isSidechain === true;
1787
+ const contribution = buildContribution(usage, model, sidechain, tsMs);
1788
+ if (messageId === null) {
1789
+ agg.unkeyedResponses++;
1790
+ acceptContribution(agg, contribution);
1791
+ return;
1792
+ }
1793
+ if (existing === void 0) {
1794
+ agg.distinctResponses++;
1795
+ acceptContribution(agg, contribution);
1796
+ agg.seen.set(messageId, { requestId, contribution });
1797
+ return;
1798
+ }
1799
+ if (isReplay) agg.realReplaysFolded++;
1800
+ else agg.continuationsFolded++;
1801
+ if (!supersedes(contribution, existing.contribution)) return;
1802
+ agg.supersededByLarger++;
1803
+ retractContribution(agg, existing.contribution);
1804
+ acceptContribution(agg, contribution);
1805
+ agg.seen.set(messageId, { requestId: existing.requestId, contribution });
1806
+ }
1807
+ function acceptContribution(agg, c) {
1808
+ applyContribution(agg, c, 1);
1809
+ }
1810
+ function retractContribution(agg, c) {
1811
+ applyContribution(agg, c, -1);
1812
+ }
1813
+ function supersedes(next, prev) {
1814
+ if (prev.sidechain !== next.sidechain)
1815
+ return prev.sidechain && !next.sidechain;
1816
+ return next.total > prev.total;
1817
+ }
1818
+ function readCounts(usage) {
1819
+ const t = {
1820
+ input: asNum(usage.input_tokens),
1821
+ output: asNum(usage.output_tokens),
1822
+ cacheWrite5m: 0,
1823
+ cacheWrite1h: 0,
1824
+ cacheWriteUnsplit: 0,
1825
+ cacheRead: asNum(usage.cache_read_input_tokens)
1826
+ };
1827
+ const cacheWriteTotal = asNum(usage.cache_creation_input_tokens);
1828
+ const cc = asObj(usage.cache_creation);
1829
+ if (cc) {
1830
+ t.cacheWrite5m = asNum(cc.ephemeral_5m_input_tokens);
1831
+ t.cacheWrite1h = asNum(cc.ephemeral_1h_input_tokens);
1832
+ const residual = cacheWriteTotal - (t.cacheWrite5m + t.cacheWrite1h);
1833
+ if (residual > 0) t.cacheWriteUnsplit = residual;
1834
+ } else {
1835
+ t.cacheWriteUnsplit = cacheWriteTotal;
1836
+ }
1837
+ return t;
1838
+ }
1839
+ function modelKeyFor(model, speed) {
1840
+ return normalizeModel(speed === "fast" ? `${model}#fast` : model);
1841
+ }
1842
+ function makeEntry(modelKey, counts, tsMs) {
1843
+ return {
1844
+ modelKey,
1845
+ counts,
1846
+ costUSD: apiEquivalentCost(modelKey, counts, tsMs)
1847
+ };
1848
+ }
1849
+ function buildContribution(usage, model, sidechain, tsMs) {
1850
+ const modelKey = modelKeyFor(model, asStr(usage.speed));
1851
+ const entries = [makeEntry(modelKey, readCounts(usage), tsMs)];
1852
+ const mirrored = /* @__PURE__ */ new Map();
1853
+ let fallbackAttempts = 0;
1854
+ let untypedMirrors = 0;
1855
+ for (const rawIt of asArr(usage.iterations)) {
1856
+ const it = asObj(rawIt);
1857
+ if (!it) continue;
1858
+ const itType = asName(it.type) ?? "(untyped)";
1859
+ const itModel = asName(it.model);
1860
+ const itKey = itModel === null ? null : modelKeyFor(itModel, asStr(it.speed));
1861
+ if (itType === "advisor_message") {
1862
+ entries.push(makeEntry(itKey ?? modelKey, readCounts(it), tsMs));
1863
+ continue;
1864
+ }
1865
+ if (itKey === null) {
1866
+ untypedMirrors++;
1867
+ bump(mirrored, itType);
1868
+ continue;
1869
+ }
1870
+ if (itKey === modelKey) {
1871
+ bump(mirrored, itType);
1872
+ continue;
1873
+ }
1874
+ entries.push(makeEntry(itKey, readCounts(it), tsMs));
1875
+ fallbackAttempts++;
1876
+ }
1877
+ const serverTools = asObj(usage.server_tool_use);
1878
+ return {
1879
+ entries,
1880
+ total: entries.reduce((a, e) => a + countsTotal(e.counts), 0),
1881
+ sidechain,
1882
+ webSearch: serverTools ? asNum(serverTools.web_search_requests) : 0,
1883
+ webFetch: serverTools ? asNum(serverTools.web_fetch_requests) : 0,
1884
+ mirroredIterationTypes: [...mirrored],
1885
+ fallbackAttempts,
1886
+ untypedMirrors
1887
+ };
1888
+ }
1889
+ function applyContribution(agg, c, sign) {
1890
+ c.entries.forEach(({ modelKey, counts, costUSD }, i) => {
1891
+ let m = agg.byModel.get(modelKey);
1892
+ if (!m) {
1893
+ m = emptyUsage();
1894
+ agg.byModel.set(modelKey, m);
1895
+ }
1896
+ if (i === 0) m.messages += sign;
1897
+ m.input += sign * counts.input;
1898
+ m.output += sign * counts.output;
1899
+ m.cacheWrite5m += sign * counts.cacheWrite5m;
1900
+ m.cacheWrite1h += sign * counts.cacheWrite1h;
1901
+ m.cacheWriteUnsplit += sign * counts.cacheWriteUnsplit;
1902
+ m.cacheRead += sign * counts.cacheRead;
1903
+ if (costUSD === null) m.unpricedTokens += sign * countsTotal(counts);
1904
+ else m.costUSD += sign * costUSD;
1905
+ });
1906
+ if (c.sidechain) agg.sidechainTokens += sign * c.total;
1907
+ else agg.mainTokens += sign * c.total;
1908
+ agg.webSearchRequests += sign * c.webSearch;
1909
+ agg.webFetchRequests += sign * c.webFetch;
1910
+ agg.fallbackAttempts += sign * c.fallbackAttempts;
1911
+ agg.untypedMirrors += sign * c.untypedMirrors;
1912
+ for (const [type, count] of c.mirroredIterationTypes) {
1913
+ bump(agg.mirroredIterationTypes, type, sign * count);
1914
+ }
1915
+ }
1916
+ function ingestContentBlocks(agg, content) {
1917
+ for (const rawBlock of asArr(content)) {
1918
+ const block = asObj(rawBlock);
1919
+ if (!block) continue;
1920
+ const type = asStr(block.type);
1921
+ if (type === "thinking") agg.thinkingBlocks++;
1922
+ else if (type === "text") agg.textBlocks++;
1923
+ else if (type === "tool_use") ingestToolUse(agg, block);
1924
+ }
1925
+ }
1926
+ function ingestToolUse(agg, block) {
1927
+ const name = asName(block.name);
1928
+ if (!name) return;
1929
+ const blockId = asStr(block.id);
1930
+ if (!blockId) {
1931
+ agg.toolBlocksWithoutId++;
1932
+ return;
1933
+ }
1934
+ if (agg.toolCallDedup.has(blockId)) return;
1935
+ agg.toolCallDedup.add(blockId);
1936
+ const input = asObj(block.input) ?? {};
1937
+ if (name.startsWith("mcp__")) {
1938
+ const parts = name.slice("mcp__".length).split("__");
1939
+ bump(agg.mcpServerCalls, parts[0] || "(unknown)");
1940
+ bump(agg.mcpToolCalls, name);
1941
+ return;
1942
+ }
1943
+ if (name === "Skill") {
1944
+ bump(agg.skillCalls, asName(input.skill) ?? "(unnamed)");
1945
+ bump(agg.toolCalls, "Skill");
1946
+ return;
1947
+ }
1948
+ if (name === "Agent" || name === "Task") {
1949
+ bump(agg.subagentCalls, asName(input.subagent_type) ?? "(default)");
1950
+ bump(agg.toolCalls, "Agent");
1951
+ return;
1952
+ }
1953
+ bump(agg.toolCalls, name);
1954
+ }
1955
+ var SLASH_RE = /<command-name>\/?([^<\n\r]{1,64})<\/command-name>/g;
1956
+ function ingestUser(agg, rec) {
1957
+ const msg = asObj(rec.message);
1958
+ if (!msg) return;
1959
+ const content = msg.content;
1960
+ let text = "";
1961
+ if (typeof content === "string") text = content;
1962
+ else {
1963
+ for (const rawBlock of asArr(content)) {
1964
+ const block = asObj(rawBlock);
1965
+ if (!block) continue;
1966
+ if (asStr(block.type) === "text") text += asStr(block.text) ?? "";
1967
+ }
1968
+ }
1969
+ if (!text.includes("<command-name>")) return;
1970
+ for (const match of text.matchAll(SLASH_RE)) {
1971
+ bump(agg.slashCommands, cleanName(match[1]));
1972
+ }
1973
+ }
1974
+ function buildModelRows(agg) {
1975
+ const rows = [];
1976
+ let totalTokens = 0;
1977
+ let totalCostUSD = 0;
1978
+ const unpricedModels = [];
1979
+ let unpricedTokens = 0;
1980
+ for (const [modelKey, u] of agg.byModel) {
1981
+ const tokens = {
1982
+ input: u.input,
1983
+ output: u.output,
1984
+ cacheWrite5m: u.cacheWrite5m,
1985
+ cacheWrite1h: u.cacheWrite1h,
1986
+ cacheWriteUnsplit: u.cacheWriteUnsplit,
1987
+ cacheRead: u.cacheRead
1988
+ };
1989
+ const sum = countsTotal(tokens);
1990
+ totalTokens += sum;
1991
+ if (u.unpricedTokens > 0) {
1992
+ unpricedModels.push(modelKey);
1993
+ unpricedTokens += u.unpricedTokens;
1994
+ }
1995
+ totalCostUSD += u.costUSD;
1996
+ rows.push({
1997
+ modelKey,
1998
+ tokens,
1999
+ totalTokens: sum,
2000
+ messages: u.messages,
2001
+ share: 0,
2002
+ // A model we hold no rate for at all reports null rather than $0.00,
2003
+ // so "we can't price this" never reads as "this was free".
2004
+ costUSD: isPricedModel(modelKey) ? u.costUSD : null,
2005
+ unpricedTokens: u.unpricedTokens
2006
+ });
2007
+ }
2008
+ for (const r of rows) r.share = totalTokens ? r.totalTokens / totalTokens : 0;
2009
+ rows.sort(
2010
+ (a, b) => b.totalTokens - a.totalTokens || a.modelKey.localeCompare(b.modelKey)
2011
+ );
2012
+ return { rows, totalTokens, totalCostUSD, unpricedModels, unpricedTokens };
2013
+ }
2014
+ function computeCacheHitShare(rows) {
2015
+ let cacheRead = 0;
2016
+ let inputClass = 0;
2017
+ for (const r of rows) {
2018
+ cacheRead += r.tokens.cacheRead;
2019
+ inputClass += r.tokens.input + r.tokens.cacheRead + r.tokens.cacheWrite5m + r.tokens.cacheWrite1h + r.tokens.cacheWriteUnsplit;
2020
+ }
2021
+ return inputClass ? cacheRead / inputClass : 0;
2022
+ }
2023
+ function newestVersion(versions) {
2024
+ let best = null;
2025
+ let bestParts = [];
2026
+ for (const v of versions) {
2027
+ const parts = v.split(".").map((p7) => Number.parseInt(p7, 10));
2028
+ if (parts.some((n) => !Number.isFinite(n))) continue;
2029
+ if (best === null || compareParts(parts, bestParts) > 0) {
2030
+ best = v;
2031
+ bestParts = parts;
2032
+ }
2033
+ }
2034
+ return best;
2035
+ }
2036
+ function compareParts(a, b) {
2037
+ const len = Math.max(a.length, b.length);
2038
+ for (let i = 0; i < len; i++) {
2039
+ const d = (a[i] ?? 0) - (b[i] ?? 0);
2040
+ if (d !== 0) return d;
2041
+ }
2042
+ return 0;
2043
+ }
2044
+ function finalize(agg) {
2045
+ const { rows, totalTokens, totalCostUSD, unpricedModels, unpricedTokens } = buildModelRows(agg);
2046
+ const byCount = (m) => [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
2047
+ let totalToolCalls = 0;
2048
+ for (const v of agg.toolCalls.values()) totalToolCalls += v;
2049
+ for (const v of agg.mcpToolCalls.values()) totalToolCalls += v;
2050
+ const sideTotal = agg.sidechainTokens + agg.mainTokens;
2051
+ return {
2052
+ models: rows,
2053
+ totalTokens,
2054
+ totalCostUSD,
2055
+ unpricedModels,
2056
+ unpricedTokens,
2057
+ cacheHitShare: computeCacheHitShare(rows),
2058
+ sidechainShare: sideTotal ? agg.sidechainTokens / sideTotal : 0,
2059
+ activeDays: agg.activeDays.size,
2060
+ firstTs: agg.firstTs,
2061
+ lastTs: agg.lastTs,
2062
+ sessions: agg.sessions.size,
2063
+ projects: agg.projectDirs.size,
2064
+ tools: byCount(agg.toolCalls),
2065
+ skills: byCount(agg.skillCalls),
2066
+ mcpServers: byCount(agg.mcpServerCalls),
2067
+ subagents: byCount(agg.subagentCalls),
2068
+ slashCommands: byCount(agg.slashCommands),
2069
+ totalToolCalls,
2070
+ harnessVersion: newestVersion(agg.ccVersions)
2071
+ };
2072
+ }
2073
+
2074
+ // src/transcripts/bundled-allowlist.ts
2075
+ var BUILTIN_SUBAGENTS = [
2076
+ "(default)",
2077
+ "claude",
2078
+ "claude-code-guide",
2079
+ "Explore",
2080
+ "fork",
2081
+ "general-purpose",
2082
+ "Plan",
2083
+ "statusline-setup"
2084
+ ];
2085
+ var BUILTIN_SKILLS = [
2086
+ "artifact-capabilities",
2087
+ "artifact-design",
2088
+ "claude-api",
2089
+ "code-review",
2090
+ "codebase-design",
2091
+ "dataviz",
2092
+ "diagnosing-bugs",
2093
+ "domain-modeling",
2094
+ "fewer-permission-prompts",
2095
+ "grilling",
2096
+ "init",
2097
+ "keybindings-help",
2098
+ "loop",
2099
+ "prototype",
2100
+ "research",
2101
+ "review",
2102
+ "run",
2103
+ "schedule",
2104
+ "security-review",
2105
+ "simplify",
2106
+ "tdd",
2107
+ "update-config"
2108
+ ];
2109
+ var BUILTIN_SLASH_COMMANDS = [
2110
+ "add-dir",
2111
+ "agents",
2112
+ "bug",
2113
+ "clear",
2114
+ "compact",
2115
+ "config",
2116
+ "context",
2117
+ "cost",
2118
+ "doctor",
2119
+ "effort",
2120
+ "exit",
2121
+ "export",
2122
+ "fast",
2123
+ "help",
2124
+ "hooks",
2125
+ "ide",
2126
+ "init",
2127
+ "login",
2128
+ "logout",
2129
+ "mcp",
2130
+ "memory",
2131
+ "model",
2132
+ "output-style",
2133
+ "permissions",
2134
+ "plugin",
2135
+ "privacy-settings",
2136
+ "release-notes",
2137
+ "resume",
2138
+ "review",
2139
+ "rewind",
2140
+ "security-review",
2141
+ "status",
2142
+ "statusline",
2143
+ "terminal-setup",
2144
+ "todos",
2145
+ "upgrade",
2146
+ "usage",
2147
+ "vim",
2148
+ "workflows"
2149
+ ];
2150
+ var PUBLIC_MCP_SERVERS = [
2151
+ "chrome-devtools",
2152
+ "context7",
2153
+ "deepwiki",
2154
+ "figma",
2155
+ "filesystem",
2156
+ "git",
2157
+ "github",
2158
+ "huggingface",
2159
+ "ide",
2160
+ "linear",
2161
+ "notion",
2162
+ "playwright",
2163
+ "puppeteer",
2164
+ "sentry",
2165
+ "slack",
2166
+ "stripe"
2167
+ ];
2168
+ var BUNDLED_CURATED_ALLOWLIST = {
2169
+ mcpServers: PUBLIC_MCP_SERVERS,
2170
+ skills: BUILTIN_SKILLS,
2171
+ subagents: BUILTIN_SUBAGENTS,
2172
+ slashCommands: BUILTIN_SLASH_COMMANDS
2173
+ };
2174
+
2175
+ // src/transcripts/allowlist.ts
2176
+ var BUILTIN_TOOLS = /* @__PURE__ */ new Set([
2177
+ "Agent",
2178
+ "Artifact",
2179
+ "AskUserQuestion",
2180
+ "Bash",
2181
+ "BashOutput",
2182
+ "CronCreate",
2183
+ "CronDelete",
2184
+ "CronList",
2185
+ "DesignSync",
2186
+ "Edit",
2187
+ "EndConversation",
2188
+ "EnterPlanMode",
2189
+ "EnterWorktree",
2190
+ "ExitPlanMode",
2191
+ "ExitWorktree",
2192
+ "Glob",
2193
+ "Grep",
2194
+ "KillBash",
2195
+ "KillShell",
2196
+ "ListMcpResourcesTool",
2197
+ "LS",
2198
+ "Monitor",
2199
+ "MultiEdit",
2200
+ "NotebookEdit",
2201
+ "NotebookRead",
2202
+ "PushNotification",
2203
+ "Read",
2204
+ "ReadMcpResourceDirTool",
2205
+ "ReadMcpResourceTool",
2206
+ "RemoteTrigger",
2207
+ "ReportFindings",
2208
+ "ScheduleWakeup",
2209
+ "SendMessage",
2210
+ "SendUserFile",
2211
+ "Skill",
2212
+ "SlashCommand",
2213
+ "Task",
2214
+ "TaskCreate",
2215
+ "TaskGet",
2216
+ "TaskList",
2217
+ "TaskOutput",
2218
+ "TaskStop",
2219
+ "TaskUpdate",
2220
+ "TodoWrite",
2221
+ "ToolSearch",
2222
+ "WebFetch",
2223
+ "WebSearch",
2224
+ "Workflow",
2225
+ "Write"
2226
+ ]);
2227
+ var NAME_CATEGORIES = [
2228
+ "builtinTools",
2229
+ "mcpServers",
2230
+ "skills",
2231
+ "subagents",
2232
+ "slashCommands"
2233
+ ];
2234
+ var EMPTY_OPT_INS = {
2235
+ builtinTools: [],
2236
+ mcpServers: [],
2237
+ skills: [],
2238
+ subagents: [],
2239
+ slashCommands: []
2240
+ };
2241
+ var BUNDLED_SYNC_CONFIG = {
2242
+ allowlist: BUNDLED_CURATED_ALLOWLIST,
2243
+ publishCost: false,
2244
+ // Empty for the same reason, and it is the load-bearing half of #42
2245
+ // decision 2: a failed config fetch reverts every ticked name to
2246
+ // kept-private. Losing the network publishes LESS, never more.
2247
+ optIns: EMPTY_OPT_INS,
2248
+ // Same direction again (#48): a machine that cannot read the switch does not
2249
+ // upload the names it is holding back. The default is ON server-side, so this
2250
+ // costs the owner one retry and never costs them a name.
2251
+ reviewKeptPrivate: false,
2252
+ // No fetch, no destination — and the gate refuses to publish without one.
2253
+ stack: null
2254
+ };
2255
+ var SYNC_CONFIG_PATH = "/api/sync-config";
2256
+ var FETCH_TIMEOUT_MS = 5e3;
2257
+ var CURATED_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9 ._:@/-]{0,63}$/;
2258
+ function readNameList(v) {
2259
+ if (!Array.isArray(v)) return [];
2260
+ const out = [];
2261
+ for (const item of v) {
2262
+ if (typeof item === "string" && CURATED_NAME_RE.test(item)) out.push(item);
2263
+ }
2264
+ return out;
2265
+ }
2266
+ function readOptInList(v) {
2267
+ if (!Array.isArray(v)) return [];
2268
+ const out = [];
2269
+ for (const item of v) {
2270
+ if (typeof item === "string" && isDisplaySafeName(item)) out.push(item);
2271
+ }
2272
+ return out;
2273
+ }
2274
+ function readOptIns(v) {
2275
+ if (typeof v !== "object" || v === null || Array.isArray(v))
2276
+ return EMPTY_OPT_INS;
2277
+ const obj = v;
2278
+ return {
2279
+ builtinTools: readOptInList(obj.builtinTools),
2280
+ mcpServers: readOptInList(obj.mcpServers),
2281
+ skills: readOptInList(obj.skills),
2282
+ subagents: readOptInList(obj.subagents),
2283
+ slashCommands: readOptInList(obj.slashCommands)
2284
+ };
2285
+ }
2286
+ var STACK_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
2287
+ function readStack(v) {
2288
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return null;
2289
+ const obj = v;
2290
+ if (typeof obj.name !== "string" || !isDisplaySafeName(obj.name)) return null;
2291
+ if (typeof obj.slug !== "string" || !STACK_SLUG_RE.test(obj.slug))
2292
+ return null;
2293
+ return { name: obj.name, slug: obj.slug };
2294
+ }
2295
+ function readSyncConfig(raw) {
2296
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
2297
+ return null;
2298
+ const obj = raw;
2299
+ const listRaw = obj.allowlist;
2300
+ if (typeof listRaw !== "object" || listRaw === null) return null;
2301
+ const list = listRaw;
2302
+ return {
2303
+ allowlist: {
2304
+ mcpServers: readNameList(list.mcpServers),
2305
+ skills: readNameList(list.skills),
2306
+ subagents: readNameList(list.subagents),
2307
+ slashCommands: readNameList(list.slashCommands)
2308
+ },
2309
+ // Anything other than an explicit `true` fails closed.
2310
+ publishCost: obj.publishCost === true,
2311
+ // Absent means "no stack resolved" — an anonymous fetch, or a token bound
2312
+ // to nothing. Both fail closed to publishing no user-chosen names.
2313
+ optIns: readOptIns(obj.optIns),
2314
+ // Anything other than an explicit `true` keeps the names on the machine.
2315
+ reviewKeptPrivate: obj.reviewKeptPrivate === true,
2316
+ stack: readStack(obj.stack)
2317
+ };
2318
+ }
2319
+ async function loadSyncConfig(opts) {
2320
+ const doFetch = opts.fetchImpl ?? fetch;
2321
+ try {
2322
+ const res = await doFetch(`${opts.baseUrl}${SYNC_CONFIG_PATH}`, {
2323
+ signal: AbortSignal.timeout(opts.timeoutMs ?? FETCH_TIMEOUT_MS),
2324
+ headers: {
2325
+ Accept: "application/json",
2326
+ ...opts.token ? { Authorization: `Bearer ${opts.token}` } : {}
2327
+ }
2328
+ });
2329
+ if (!res.ok) {
2330
+ return {
2331
+ config: BUNDLED_SYNC_CONFIG,
2332
+ source: "bundled",
2333
+ error: `sync-config returned ${res.status}`
2334
+ };
2335
+ }
2336
+ const parsed = readSyncConfig(await res.json());
2337
+ if (!parsed) {
2338
+ return {
2339
+ config: BUNDLED_SYNC_CONFIG,
2340
+ source: "bundled",
2341
+ error: "sync-config response was not the expected shape"
2342
+ };
2343
+ }
2344
+ return { config: parsed, source: "fetched" };
2345
+ } catch (err) {
2346
+ return {
2347
+ config: BUNDLED_SYNC_CONFIG,
2348
+ source: "bundled",
2349
+ error: err instanceof Error ? err.message : "sync-config fetch failed"
2350
+ };
2351
+ }
2352
+ }
2353
+ var PLUGIN_MCP_RE = /^plugin_([^_]+)_(.+)$/;
2354
+ var PLUGIN_PREFIX_RE = /^([^:\s]+):(.+)$/;
2355
+ function pluginGroup(name) {
2356
+ return PLUGIN_MCP_RE.exec(name)?.[1] ?? PLUGIN_PREFIX_RE.exec(name)?.[1] ?? null;
2357
+ }
2358
+ function publishedName(name, sets) {
2359
+ if (sets.publishable.has(name)) return name;
2360
+ const inner = PLUGIN_MCP_RE.exec(name)?.[2];
2361
+ if (inner && sets.curated.has(inner)) return inner;
2362
+ return null;
2363
+ }
2364
+ function filterAtoms(atoms, sets) {
2365
+ const merged = /* @__PURE__ */ new Map();
2366
+ const keptPrivate = [];
2367
+ for (const atom of atoms) {
2368
+ const published = publishedName(atom.name, sets);
2369
+ if (published === null) {
2370
+ keptPrivate.push({
2371
+ name: atom.name,
2372
+ count: atom.count,
2373
+ group: pluginGroup(atom.name)
2374
+ });
2375
+ continue;
2376
+ }
2377
+ merged.set(published, (merged.get(published) ?? 0) + atom.count);
2378
+ }
2379
+ const allowed = [...merged].map(([name, count]) => ({ name, count }));
2380
+ allowed.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
2381
+ keptPrivate.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
2382
+ return { allowed, keptPrivate, withheld: keptPrivate.length };
2383
+ }
2384
+
2385
+ // src/transcripts/scan.ts
2386
+ import { createReadStream } from "fs";
2387
+ import { readdir, realpath, stat } from "fs/promises";
2388
+ import { homedir as homedir7 } from "os";
2389
+ import path from "path";
2390
+ import readline from "readline";
2391
+ function transcriptRoots() {
2392
+ const env = process.env.CLAUDE_CONFIG_DIR;
2393
+ if (env) {
2394
+ return env.split(",").map((s) => s.trim()).filter(Boolean).map((s) => path.join(s, "projects"));
2395
+ }
2396
+ const roots = [path.join(homedir7(), ".claude", "projects")];
2397
+ const xdg = process.env.XDG_CONFIG_HOME ?? path.join(homedir7(), ".config");
2398
+ roots.push(path.join(xdg, "claude", "projects"));
2399
+ return roots;
2400
+ }
2401
+ function windowStartMs(now, days) {
2402
+ const startOfToday = Date.UTC(
2403
+ new Date(now).getUTCFullYear(),
2404
+ new Date(now).getUTCMonth(),
2405
+ new Date(now).getUTCDate()
2406
+ );
2407
+ return startOfToday - (days - 1) * 864e5;
2408
+ }
2409
+ async function* walkJsonl(dir) {
2410
+ let entries;
2411
+ try {
2412
+ entries = await readdir(dir, { withFileTypes: true });
2413
+ } catch {
2414
+ return;
2415
+ }
2416
+ for (const e of entries) {
2417
+ const full = path.join(dir, e.name);
2418
+ if (e.isDirectory()) yield* walkJsonl(full);
2419
+ else if (e.isFile() && e.name.endsWith(".jsonl")) yield full;
2420
+ }
2421
+ }
2422
+ async function scan(agg, opts = {}) {
2423
+ const stats = {
2424
+ filesFound: 0,
2425
+ filesRead: 0,
2426
+ filesSkippedByMtime: 0,
2427
+ filesSkippedAsDuplicate: 0,
2428
+ filesUnreadable: 0
2429
+ };
2430
+ const visited = /* @__PURE__ */ new Set();
2431
+ for (const root of opts.roots ?? transcriptRoots()) {
2432
+ if (!await exists(root)) continue;
2433
+ for await (const file of walkJsonl(root)) {
2434
+ stats.filesFound++;
2435
+ let resolved;
2436
+ try {
2437
+ resolved = await realpath(file);
2438
+ } catch {
2439
+ resolved = file;
2440
+ }
2441
+ if (visited.has(resolved)) {
2442
+ stats.filesSkippedAsDuplicate++;
2443
+ continue;
2444
+ }
2445
+ visited.add(resolved);
2446
+ if (opts.sinceMs !== void 0) {
2447
+ try {
2448
+ const st = await stat(file);
2449
+ if (st.mtimeMs < opts.sinceMs) {
2450
+ stats.filesSkippedByMtime++;
2451
+ continue;
2452
+ }
2453
+ } catch {
2454
+ }
2455
+ }
2456
+ const rel = path.relative(root, file);
2457
+ const projectDir = rel.split(path.sep)[0] ?? "(root)";
2458
+ agg.files++;
2459
+ stats.filesRead++;
2460
+ if (opts.onProgress && agg.files % 200 === 0) opts.onProgress(agg.files);
2461
+ try {
2462
+ await ingestFile(agg, file, projectDir, opts.sinceMs);
2463
+ } catch {
2464
+ stats.filesUnreadable++;
2465
+ stats.filesRead--;
2466
+ }
2467
+ }
2468
+ }
2469
+ return stats;
2470
+ }
2471
+ async function exists(p7) {
2472
+ try {
2473
+ await stat(p7);
2474
+ return true;
2475
+ } catch {
2476
+ return false;
2477
+ }
2478
+ }
2479
+ async function ingestFile(agg, file, projectDir, sinceMs) {
2480
+ const rl = readline.createInterface({
2481
+ input: createReadStream(file, { encoding: "utf8" }),
2482
+ crlfDelay: Number.POSITIVE_INFINITY
2483
+ });
2484
+ for await (const line of rl) {
2485
+ if (!line) continue;
2486
+ agg.lines++;
2487
+ let rec;
2488
+ try {
2489
+ rec = JSON.parse(line);
2490
+ } catch {
2491
+ agg.parseErrors++;
2492
+ continue;
2493
+ }
2494
+ if (sinceMs !== void 0) {
2495
+ const ts = rec && typeof rec === "object" && "timestamp" in rec && typeof rec.timestamp === "string" ? Date.parse(rec.timestamp) : Number.NaN;
2496
+ if (Number.isNaN(ts) || ts < sinceMs) continue;
2497
+ }
2498
+ ingestRecord(agg, rec, { projectDir });
2499
+ }
2500
+ }
2501
+
2502
+ // src/transcripts/payload.ts
2503
+ var SCHEMA_VERSION = 1;
2504
+ var HARNESS_NAME = "claude-code";
2505
+ var MODEL_ID_UNSAFE_RE = /[^A-Za-z0-9._:-]+/g;
2506
+ var MODEL_ID_MAX = 64;
2507
+ function sanitizeModelId(id) {
2508
+ const collapsed = cleanName(id).replace(MODEL_ID_UNSAFE_RE, "-").replace(/^-+|-+$/g, "");
2509
+ if (collapsed.length === 0) return "unknown";
2510
+ return collapsed.length > MODEL_ID_MAX ? collapsed.slice(0, MODEL_ID_MAX) : collapsed;
2511
+ }
2512
+ var round4 = (n) => Math.round(n * 1e4) / 1e4;
2513
+ var round2 = (n) => Math.round(n * 100) / 100;
2514
+ var utcDate = (ms) => new Date(ms).toISOString().slice(0, 10);
2515
+ var toAtoms = (pairs) => pairs.map(([name, count]) => ({ name, count }));
2516
+ function buildCategory(observed, curated, optIns, denominator) {
2517
+ const publishable = /* @__PURE__ */ new Set([...curated, ...optIns]);
2518
+ const {
2519
+ allowed: kept,
2520
+ keptPrivate,
2521
+ withheld
2522
+ } = filterAtoms(toAtoms(observed), { publishable, curated });
2523
+ return {
2524
+ atoms: kept.map((a) => ({
2525
+ name: a.name,
2526
+ callShare: denominator ? round4(a.count / denominator) : 0
2527
+ })),
2528
+ withheld,
2529
+ keptPrivate
2530
+ };
2531
+ }
2532
+ var sumCounts = (pairs) => {
2533
+ let n = 0;
2534
+ for (const [, c] of pairs) n += c;
2535
+ return n;
2536
+ };
2537
+ function groupModels(rows) {
2538
+ const groups = /* @__PURE__ */ new Map();
2539
+ for (const r of rows) {
2540
+ const id = sanitizeModelId(baseModelId(r.modelKey));
2541
+ let g = groups.get(id);
2542
+ if (!g) {
2543
+ g = {
2544
+ id,
2545
+ totalTokens: 0,
2546
+ input: 0,
2547
+ output: 0,
2548
+ cacheWrite: 0,
2549
+ cacheRead: 0,
2550
+ costUSD: 0,
2551
+ unpricedTokens: 0,
2552
+ anyUnpriceable: false
2553
+ };
2554
+ groups.set(id, g);
2555
+ }
2556
+ g.totalTokens += r.totalTokens;
2557
+ g.input += r.tokens.input;
2558
+ g.output += r.tokens.output;
2559
+ g.cacheWrite += r.tokens.cacheWrite5m + r.tokens.cacheWrite1h + r.tokens.cacheWriteUnsplit;
2560
+ g.cacheRead += r.tokens.cacheRead;
2561
+ g.costUSD += r.costUSD ?? 0;
2562
+ g.unpricedTokens += r.unpricedTokens;
2563
+ if (r.costUSD === null) g.anyUnpriceable = true;
2564
+ }
2565
+ return [...groups.values()].sort(
2566
+ (a, b) => b.totalTokens - a.totalTokens || a.id.localeCompare(b.id)
2567
+ );
2568
+ }
2569
+ function buildModels(rows, totalTokens, publishCost) {
2570
+ return groupModels(rows).map((g) => {
2571
+ const model = {
2572
+ id: g.id,
2573
+ tokenShare: totalTokens ? round4(g.totalTokens / totalTokens) : 0,
2574
+ tokens: {
2575
+ input: g.input,
2576
+ output: g.output,
2577
+ cacheWrite: g.cacheWrite,
2578
+ cacheRead: g.cacheRead
2579
+ }
2580
+ };
2581
+ if (publishCost && !g.anyUnpriceable && g.unpricedTokens === 0) {
2582
+ model.apiEquivalentUSD = round2(g.costUSD);
2583
+ }
2584
+ return model;
2585
+ });
2586
+ }
2587
+ function buildPayload(input) {
2588
+ const { aggregate: agg, stats, syncConfig, now, windowDays } = input;
2589
+ const finalized = finalize(agg);
2590
+ const { publishCost, allowlist, optIns } = syncConfig;
2591
+ const fromMs = windowStartMs(now, windowDays);
2592
+ const from = utcDate(fromMs);
2593
+ const to = utcDate(now);
2594
+ let activeDays = 0;
2595
+ for (const d of agg.activeDays) if (d >= from && d <= to) activeDays++;
2596
+ const totalToolCalls = finalized.totalToolCalls;
2597
+ const builtins = buildCategory(
2598
+ finalized.tools,
2599
+ BUILTIN_TOOLS,
2600
+ optIns.builtinTools,
2601
+ totalToolCalls
2602
+ );
2603
+ const mcp = buildCategory(
2604
+ finalized.mcpServers,
2605
+ new Set(allowlist.mcpServers),
2606
+ optIns.mcpServers,
2607
+ sumCounts(finalized.mcpServers)
2608
+ );
2609
+ const skills = buildCategory(
2610
+ finalized.skills,
2611
+ new Set(allowlist.skills),
2612
+ optIns.skills,
2613
+ sumCounts(finalized.skills)
2614
+ );
2615
+ const subagents = buildCategory(
2616
+ finalized.subagents,
2617
+ new Set(allowlist.subagents),
2618
+ optIns.subagents,
2619
+ sumCounts(finalized.subagents)
2620
+ );
2621
+ const slash = buildCategory(
2622
+ finalized.slashCommands,
2623
+ new Set(allowlist.slashCommands),
2624
+ optIns.slashCommands,
2625
+ sumCounts(finalized.slashCommands)
2626
+ );
2627
+ const payload = {
2628
+ schemaVersion: SCHEMA_VERSION,
2629
+ capturedAt: now,
2630
+ window: { days: windowDays, from, to },
2631
+ harness: {
2632
+ name: HARNESS_NAME,
2633
+ version: finalized.harnessVersion === null ? null : sanitizeModelId(finalized.harnessVersion)
2634
+ },
2635
+ pricingTable: publishCost ? PRICING_TABLE_VERSION : null,
2636
+ activity: {
2637
+ sessions: finalized.sessions,
2638
+ activeDays,
2639
+ projects: finalized.projects,
2640
+ totalTokens: finalized.totalTokens,
2641
+ cacheHitShare: round4(finalized.cacheHitShare),
2642
+ subagentShare: round4(finalized.sidechainShare)
2643
+ },
2644
+ models: buildModels(finalized.models, finalized.totalTokens, publishCost),
2645
+ inventory: {
2646
+ builtinTools: builtins.atoms,
2647
+ mcpServers: mcp.atoms,
2648
+ skills: skills.atoms,
2649
+ subagents: subagents.atoms,
2650
+ slashCommands: slash.atoms,
2651
+ withheld: {
2652
+ builtinTools: builtins.withheld,
2653
+ mcpServers: mcp.withheld,
2654
+ skills: skills.withheld,
2655
+ subagents: subagents.withheld,
2656
+ slashCommands: slash.withheld
2657
+ }
2658
+ },
2659
+ coverage: {
2660
+ filesScanned: stats.filesRead,
2661
+ filesUnreadable: stats.filesUnreadable,
2662
+ linesParsed: agg.lines - agg.parseErrors,
2663
+ linesFailed: agg.parseErrors
2664
+ },
2665
+ excludedTokens: {
2666
+ unpriced: finalized.unpricedTokens,
2667
+ synthetic: agg.syntheticTokens
2668
+ }
2669
+ };
2670
+ return {
2671
+ payload,
2672
+ finalized,
2673
+ keptPrivate: {
2674
+ builtinTools: builtins.keptPrivate,
2675
+ mcpServers: mcp.keptPrivate,
2676
+ skills: skills.keptPrivate,
2677
+ subagents: subagents.keptPrivate,
2678
+ slashCommands: slash.keptPrivate
2679
+ }
2680
+ };
2681
+ }
2682
+ function buildSyncBody(built, syncConfig) {
2683
+ if (!syncConfig.reviewKeptPrivate) return { payload: built.payload };
2684
+ return { payload: built.payload, keptPrivate: built.keptPrivate };
2685
+ }
2686
+
2687
+ // src/transcripts/index.ts
2688
+ var DEFAULT_WINDOW_DAYS = 30;
2689
+
2690
+ // src/sync/summary.ts
2691
+ function fmtTokens(n) {
2692
+ const sig = (v) => {
2693
+ const s = v.toPrecision(3);
2694
+ return s.includes(".") ? s.replace(/\.?0+$/, "") : s;
2695
+ };
2696
+ if (n >= 1e9) return `${sig(n / 1e9)}B`;
2697
+ if (n >= 1e6) return `${sig(n / 1e6)}M`;
2698
+ if (n >= 1e3) return `${sig(n / 1e3)}k`;
2699
+ return String(n);
2700
+ }
2701
+ function fmtUSD(n) {
2702
+ return `\u2248$${Math.round(n).toLocaleString("en-US")}`;
2703
+ }
2704
+ var fmtPct = (share) => `${(share * 100).toFixed(1)}%`;
2705
+ function totalUSD(payload) {
2706
+ if (payload.pricingTable === null) return null;
2707
+ let sum = 0;
2708
+ let any = false;
2709
+ for (const m of payload.models) {
2710
+ if (m.apiEquivalentUSD !== void 0) {
2711
+ sum += m.apiEquivalentUSD;
2712
+ any = true;
2713
+ }
2714
+ }
2715
+ return any ? sum : null;
2716
+ }
2717
+ function withheldCount(payload) {
2718
+ const w = payload.inventory.withheld;
2719
+ return w.builtinTools + w.mcpServers + w.skills + w.subagents + w.slashCommands;
2720
+ }
2721
+ function buildGateDialog(ctx) {
2722
+ const { payload, keptPrivate } = ctx.body;
2723
+ const usd = totalUSD(payload);
2724
+ const facts = [
2725
+ `${fmtTokens(payload.activity.totalTokens)} tokens`,
2726
+ `${payload.window.days} days`,
2727
+ ...usd === null ? [] : [fmtUSD(usd)]
2728
+ ].join(" \xB7 ");
2729
+ const n = withheldCount(payload);
2730
+ const lines2 = [`Publish to aistack? ${facts}`];
2731
+ if (n > 0) {
2732
+ lines2.push(
2733
+ keptPrivate === void 0 ? `${n} name${n === 1 ? "" : "s"} stay${n === 1 ? "s" : ""} on this machine` : `${n} name${n === 1 ? "" : "s"} go${n === 1 ? "es" : ""} up for you to review`
2734
+ );
2735
+ }
2736
+ return lines2.join("\n");
2737
+ }
2738
+ var CATEGORY_LABEL = {
2739
+ builtinTools: "tools",
2740
+ mcpServers: "mcp",
2741
+ skills: "skills",
2742
+ subagents: "agents",
2743
+ slashCommands: "commands"
2744
+ };
2745
+ function keptPrivateRows(keptPrivate) {
2746
+ const groups = /* @__PURE__ */ new Map();
2747
+ const singles = [];
2748
+ for (const category of NAME_CATEGORIES) {
2749
+ for (const atom of keptPrivate[category]) {
2750
+ if (atom.group === null) singles.push(atom.name);
2751
+ else groups.set(atom.group, (groups.get(atom.group) ?? 0) + 1);
2752
+ }
2753
+ }
2754
+ const rows = [...groups].map(([label, names]) => ({ label, names }));
2755
+ for (const name of singles) rows.push({ label: name, names: 1 });
2756
+ rows.sort((a, b) => b.names - a.names || a.label.localeCompare(b.label));
2757
+ return rows;
2758
+ }
2759
+ var KEPT_PRIVATE_ROWS_SHOWN = 6;
2760
+ function buildGateSummary(ctx) {
2761
+ const { body, keptPrivate, config, source, baseUrl } = ctx;
2762
+ const { payload } = body;
2763
+ const host = baseUrl.replace(/^https?:\/\//, "");
2764
+ const out = [];
2765
+ out.push("from your machine \u2014 sync preview");
2766
+ out.push("");
2767
+ if (config.stack === null) {
2768
+ out.push("to (no linked stack \u2014 publish is unavailable)");
2769
+ } else {
2770
+ out.push(
2771
+ `to ${config.stack.name} \xB7 ${host}/stacks/${config.stack.slug}`
2772
+ );
2773
+ }
2774
+ out.push(
2775
+ `window ${payload.window.days} days \xB7 ${payload.window.from} \u2192 ${payload.window.to}`
2776
+ );
2777
+ out.push(
2778
+ `activity ${payload.activity.sessions} sessions \xB7 ${payload.activity.activeDays} active days \xB7 ${fmtTokens(payload.activity.totalTokens)} tokens`
2779
+ );
2780
+ const usd = totalUSD(payload);
2781
+ out.push(
2782
+ usd === null ? "cost not published" : `cost ${fmtUSD(usd)} at API prices`
2783
+ );
2784
+ const cov = payload.coverage;
2785
+ if (cov.filesUnreadable > 0 || cov.linesFailed > 0) {
2786
+ out.push(
2787
+ `coverage ${cov.filesUnreadable} files unreadable \xB7 ${cov.linesFailed} lines failed \u2014 this reading is a floor`
2788
+ );
2789
+ }
2790
+ out.push("");
2791
+ out.push("models");
2792
+ for (const m of payload.models) {
2793
+ const dollars = usd !== null && m.apiEquivalentUSD !== void 0 ? ` ${fmtUSD(m.apiEquivalentUSD)}` : "";
2794
+ out.push(` ${m.id.padEnd(28)} ${fmtPct(m.tokenShare)}${dollars}`);
2795
+ }
2796
+ out.push("");
2797
+ out.push("what publishes");
2798
+ for (const category of NAME_CATEGORIES) {
2799
+ const atoms = payload.inventory[category];
2800
+ if (atoms.length === 0) continue;
2801
+ const names = atoms.map((a) => a.name).join(", ");
2802
+ out.push(` ${CATEGORY_LABEL[category].padEnd(9)} ${names}`);
2803
+ }
2804
+ const n = withheldCount(payload);
2805
+ if (n > 0) {
2806
+ out.push("");
2807
+ out.push(`kept private: ${n} name${n === 1 ? "" : "s"}`);
2808
+ const rows = keptPrivateRows(keptPrivate);
2809
+ const shown = rows.slice(0, KEPT_PRIVATE_ROWS_SHOWN);
2810
+ const width = Math.max(...shown.map((r) => r.label.length));
2811
+ for (const row of shown) {
2812
+ out.push(` ${row.label.padEnd(width)} ${row.names}`);
2813
+ }
2814
+ if (rows.length > shown.length) {
2815
+ out.push(` ...${rows.length - shown.length} more`);
2816
+ }
2817
+ if (body.keptPrivate !== void 0 && config.stack !== null) {
2818
+ out.push(` publish them at ${host}/stacks/${config.stack.slug}/changes`);
2819
+ out.push(
2820
+ " (they go up for you to review \u2014 turn off: Review kept-private names, on your stack)"
2821
+ );
2822
+ } else {
2823
+ out.push(" they stay on this machine");
2824
+ }
2825
+ }
2826
+ if (source === "bundled") {
2827
+ out.push("");
2828
+ out.push(
2829
+ "! could not fetch your settings from aistack \u2014 using the bundled list."
2830
+ );
2831
+ out.push(
2832
+ " This publishes less: no cost, no ticked names, nothing staged for review."
2833
+ );
2834
+ }
2835
+ return out.join("\n");
2836
+ }
2837
+
2838
+ // src/sync/stage.ts
2839
+ function stageId(bodyJson) {
2840
+ return createHash("sha256").update(bodyJson).digest("hex").slice(0, 12);
2841
+ }
2842
+ async function stageSync(deps) {
2843
+ const now = (deps.now ?? Date.now)();
2844
+ const token = (deps.getTokenImpl ?? getToken)();
2845
+ const loadConfig = deps.loadConfigImpl ?? loadSyncConfig;
2846
+ const doScan = deps.scanImpl ?? scan;
2847
+ const windowDays = deps.windowDays ?? DEFAULT_WINDOW_DAYS;
2848
+ const { config, source } = await loadConfig({
2849
+ baseUrl: deps.baseUrl,
2850
+ ...token ? { token } : {}
2851
+ });
2852
+ const aggregate = createAggregate();
2853
+ const stats = await doScan(aggregate, {
2854
+ sinceMs: windowStartMs(now, windowDays)
2855
+ });
2856
+ const built = buildPayload({
2857
+ aggregate,
2858
+ stats,
2859
+ syncConfig: config,
2860
+ now,
2861
+ windowDays
2862
+ });
2863
+ const body = buildSyncBody(built, config);
2864
+ const bodyJson = JSON.stringify(body);
2865
+ const ctx = {
2866
+ body,
2867
+ keptPrivate: built.keptPrivate,
2868
+ config,
2869
+ source,
2870
+ baseUrl: deps.baseUrl
2871
+ };
2872
+ let blockedReason = null;
2873
+ if (token === null) {
2874
+ blockedReason = "This machine is not linked. Run `npx @use-aistack/cli login` first.";
2875
+ } else if (config.stack === null) {
2876
+ blockedReason = source === "bundled" ? "Could not fetch your settings from aistack, so the destination stack is unknown. Publish needs it. Check the network and preview again." : "The token resolves no destination stack. Run `npx @use-aistack/cli login` again to re-link this machine.";
2877
+ }
2878
+ return {
2879
+ id: stageId(bodyJson),
2880
+ bodyJson,
2881
+ body,
2882
+ keptPrivate: built.keptPrivate,
2883
+ summary: buildGateSummary(ctx),
2884
+ dialog: buildGateDialog(ctx),
2885
+ config,
2886
+ token,
2887
+ stagedAt: now,
2888
+ blockedReason
2889
+ };
2890
+ }
2891
+
2892
+ // src/commands/sync.ts
2893
+ async function syncCommand() {
2894
+ intro2("sync");
2895
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2896
+ outroError("sync needs an interactive terminal \u2014 nothing was sent");
2897
+ process.exitCode = 1;
2898
+ return;
2899
+ }
2900
+ const s = p6.spinner();
2901
+ s.start("Scanning local Claude Code transcripts");
2902
+ let staged;
2903
+ try {
2904
+ staged = await stageSync({ baseUrl: BASE_URL });
2905
+ } catch (e) {
2906
+ s.stop("Scan failed");
2907
+ outroError(e instanceof Error ? e.message : String(e));
2908
+ process.exitCode = 1;
2909
+ return;
2910
+ }
2911
+ s.stop("Scan complete");
2912
+ p6.log.message(staged.summary.split("\n").join("\n"));
2913
+ if (staged.blockedReason !== null) {
2914
+ outroError(staged.blockedReason);
2915
+ process.exitCode = 1;
2916
+ return;
2917
+ }
2918
+ const decision = await p6.select({
2919
+ message: staged.dialog.split("\n").join(dim(" \xB7 ")),
2920
+ options: [
2921
+ { value: "cancel", label: "Cancel", hint: "nothing leaves this machine" },
2922
+ { value: "publish", label: "Publish" }
2923
+ ],
2924
+ initialValue: "cancel"
2925
+ });
2926
+ if (p6.isCancel(decision) || decision !== "publish") {
2927
+ outroCancel("nothing was sent");
2928
+ return;
2929
+ }
2930
+ s.start("Publishing");
2931
+ try {
2932
+ const res = await syncPublish(staged.token, staged.bodyJson);
2933
+ s.stop("Published");
2934
+ const lines2 = [
2935
+ `Snapshot received at ${new Date(res.receivedAt).toISOString()}`,
2936
+ lime(res.url)
2937
+ ];
2938
+ if (res.keptPrivate.refused && staged.body.keptPrivate !== void 0) {
2939
+ lines2.push(
2940
+ "Note: the kept-private names were refused by the server \u2014 the review switch is off there now. They stayed on this machine."
2941
+ );
2942
+ } else if (res.keptPrivate.stored > 0) {
2943
+ lines2.push(
2944
+ `${res.keptPrivate.stored} kept-private names went up for your review at ${res.url}/changes`
2945
+ );
2946
+ }
2947
+ p6.log.message(lines2.join("\n"));
2948
+ await offerConnectUpsell();
2949
+ outro2("done");
2950
+ } catch (e) {
2951
+ s.stop("Publish failed");
2952
+ outroError(e instanceof Error ? e.message : String(e));
2953
+ process.exitCode = 1;
2954
+ }
2955
+ }
2956
+
2957
+ // src/sync/server.ts
2958
+ var SERVER_NAME = "aistack";
2959
+ var SERVER_VERSION = "0.3.0";
2960
+ var STAGE_TTL_MS = 10 * 60 * 1e3;
2961
+ var ELICIT_TIMEOUT_MS = 10 * 60 * 1e3;
2962
+ var PREVIEW_TOOL = {
2963
+ name: "sync_preview",
2964
+ description: "Scan local Claude Code transcripts and stage a measured-usage snapshot for aistack. Returns the full preview of exactly what would publish. Show the returned text to the user VERBATIM \u2014 it is the review surface. Nothing is sent.",
2965
+ inputSchema: { type: "object", properties: {} },
2966
+ annotations: {
2967
+ title: "aistack \u2014 preview sync (sends nothing)",
2968
+ readOnlyHint: true,
2969
+ openWorldHint: true
2970
+ }
2971
+ };
2972
+ var PUBLISH_TOOL = {
2973
+ name: "sync_publish",
2974
+ description: "Publish the staged aistack snapshot named by preview_id. Asks the user for confirmation during the call; only their explicit choice sends anything. Call sync_preview first and show its output.",
2975
+ inputSchema: {
2976
+ type: "object",
2977
+ properties: {
2978
+ preview_id: {
2979
+ type: "string",
2980
+ description: "The `preview id` line from sync_preview's output."
2981
+ }
2982
+ },
2983
+ required: ["preview_id"]
2984
+ },
2985
+ annotations: {
2986
+ title: "aistack \u2014 publish measured usage (asks the user first)",
2987
+ destructiveHint: false,
2988
+ openWorldHint: true
2989
+ }
2990
+ };
2991
+ var textResult = (text, isError = false) => ({
2992
+ content: [{ type: "text", text }],
2993
+ ...isError ? { isError: true } : {}
2994
+ });
2995
+ function createSyncServer(deps, send) {
2996
+ const now = deps.now ?? Date.now;
2997
+ const stage = deps.stageImpl ?? stageSync;
2998
+ const publish = deps.publishImpl ?? syncPublish;
2999
+ const log6 = deps.log ?? (() => {
3000
+ });
3001
+ const elicitTimeoutMs = deps.elicitTimeoutMs ?? ELICIT_TIMEOUT_MS;
3002
+ let clientSupportsElicitation = false;
3003
+ let staged = null;
3004
+ let nextRequestId = 1;
3005
+ const pending = /* @__PURE__ */ new Map();
3006
+ const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
3007
+ const err = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
3008
+ const request2 = (method, params, onReply) => {
3009
+ const id = `aistack-${nextRequestId++}`;
3010
+ pending.set(id, onReply);
3011
+ send({ jsonrpc: "2.0", id, method, params });
3012
+ const timer = setTimeout(() => {
3013
+ if (pending.delete(id)) onReply(null);
3014
+ }, elicitTimeoutMs);
3015
+ timer.unref?.();
3016
+ };
3017
+ const runPreview = async (id) => {
3018
+ try {
3019
+ staged = await stage({ baseUrl: deps.baseUrl, now });
3020
+ } catch (e) {
3021
+ staged = null;
3022
+ const message = e instanceof Error ? e.message : String(e);
3023
+ return ok(id, textResult(`Preview failed: ${message}`, true));
3024
+ }
3025
+ const lines2 = [staged.summary, ""];
3026
+ if (staged.blockedReason === null) {
3027
+ lines2.push(`preview id: ${staged.id}`);
3028
+ lines2.push(
3029
+ "To publish, call sync_publish with this preview id. The user confirms in a dialog during that call."
3030
+ );
3031
+ } else {
3032
+ lines2.push(`publish unavailable: ${staged.blockedReason}`);
3033
+ }
3034
+ return ok(id, textResult(lines2.join("\n")));
3035
+ };
3036
+ const runPublish = (id, args) => {
3037
+ if (!clientSupportsElicitation) {
3038
+ return ok(
3039
+ id,
3040
+ textResult(
3041
+ "Not published: this Claude Code version did not declare the elicitation capability, so the approve dialog cannot be shown. The gate never degrades silently \u2014 update Claude Code and try again.",
3042
+ true
3043
+ )
3044
+ );
3045
+ }
3046
+ const previewId = args?.preview_id;
3047
+ if (staged === null) {
3048
+ return ok(
3049
+ id,
3050
+ textResult(
3051
+ "Not published: nothing is staged. Run sync_preview first and show its output to the user.",
3052
+ true
3053
+ )
3054
+ );
3055
+ }
3056
+ if (typeof previewId !== "string" || previewId !== staged.id) {
3057
+ return ok(
3058
+ id,
3059
+ textResult(
3060
+ "Not published: preview_id does not match the staged preview. Run sync_preview again.",
3061
+ true
3062
+ )
3063
+ );
3064
+ }
3065
+ if (staged.blockedReason !== null) {
3066
+ return ok(id, textResult(`Not published: ${staged.blockedReason}`, true));
3067
+ }
3068
+ if (now() - staged.stagedAt > STAGE_TTL_MS) {
3069
+ staged = null;
3070
+ return ok(
3071
+ id,
3072
+ textResult(
3073
+ "Not published: the staged preview is older than 10 minutes. Run sync_preview again so the user reviews current bytes.",
3074
+ true
3075
+ )
3076
+ );
3077
+ }
3078
+ const approvedStage = staged;
3079
+ log6(`elicitation raised for stage ${approvedStage.id}`);
3080
+ request2(
3081
+ "elicitation/create",
3082
+ {
3083
+ message: approvedStage.dialog,
3084
+ requestedSchema: {
3085
+ type: "object",
3086
+ properties: {
3087
+ decision: {
3088
+ type: "string",
3089
+ // The enum widget is the one that works (#35, 1G). Never a boolean.
3090
+ enum: ["publish", "cancel"],
3091
+ description: "Publish the snapshot described above?"
3092
+ }
3093
+ },
3094
+ required: ["decision"]
3095
+ }
3096
+ },
3097
+ (reply) => {
3098
+ const result = reply?.result;
3099
+ const approved = result?.action === "accept" && result?.content?.decision === "publish";
3100
+ if (!approved) {
3101
+ const outcome = reply === null ? "timed out" : result?.action ?? "error";
3102
+ log6(`elicitation resolved without consent: ${outcome}`);
3103
+ return ok(
3104
+ id,
3105
+ textResult(
3106
+ `Not published: the confirmation was not accepted (${outcome}). Nothing left this machine.`
3107
+ )
3108
+ );
3109
+ }
3110
+ log6(`consent received, sending stage ${approvedStage.id}`);
3111
+ publish(approvedStage.token, approvedStage.bodyJson).then(
3112
+ (res) => {
3113
+ if (staged?.id === approvedStage.id) staged = null;
3114
+ const lines2 = [
3115
+ `Published. Snapshot received at ${new Date(res.receivedAt).toISOString()}.`,
3116
+ res.url
3117
+ ];
3118
+ const kp = approvedStage.body.keptPrivate;
3119
+ if (res.keptPrivate.refused && kp !== void 0) {
3120
+ lines2.push(
3121
+ "Note: the kept-private names were refused by the server \u2014 the review switch is off there now. They stayed on this machine."
3122
+ );
3123
+ } else if (res.keptPrivate.stored > 0) {
3124
+ lines2.push(
3125
+ `${res.keptPrivate.stored} kept-private names went up for your review at ${res.url}/changes`
3126
+ );
3127
+ }
3128
+ ok(id, textResult(lines2.join("\n")));
3129
+ },
3130
+ (e) => {
3131
+ const message = e instanceof Error ? e.message : String(e);
3132
+ ok(
3133
+ id,
3134
+ textResult(`Publish failed after consent: ${message}`, true)
3135
+ );
3136
+ }
3137
+ );
3138
+ }
3139
+ );
3140
+ };
3141
+ const handle = (msg) => {
3142
+ const { id, method, params } = msg;
3143
+ if (method === void 0 && id !== void 0 && pending.has(String(id))) {
3144
+ const onReply = pending.get(String(id));
3145
+ pending.delete(String(id));
3146
+ onReply?.(msg);
3147
+ return;
3148
+ }
3149
+ switch (method) {
3150
+ case "initialize": {
3151
+ const capabilities = params?.capabilities ?? {};
3152
+ clientSupportsElicitation = "elicitation" in capabilities;
3153
+ log6(
3154
+ `initialize: elicitation ${clientSupportsElicitation ? "declared" : "ABSENT"}`
3155
+ );
3156
+ return ok(id, {
3157
+ protocolVersion: params?.protocolVersion ?? "2025-06-18",
3158
+ capabilities: { tools: { listChanged: false } },
3159
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
3160
+ });
3161
+ }
3162
+ case "ping":
3163
+ return ok(id, {});
3164
+ case "tools/list":
3165
+ return ok(id, { tools: [PREVIEW_TOOL, PUBLISH_TOOL] });
3166
+ case "tools/call": {
3167
+ const name = params?.name;
3168
+ const args = params?.arguments;
3169
+ if (name === "sync_preview") return void runPreview(id);
3170
+ if (name === "sync_publish") return runPublish(id, args);
3171
+ return err(id, -32602, `Unknown tool: ${String(name)}`);
3172
+ }
3173
+ default:
3174
+ if (method?.startsWith("notifications/")) return;
3175
+ if (method !== void 0)
3176
+ return err(id, -32601, `Method not found: ${method}`);
3177
+ }
3178
+ };
3179
+ return { handle, staged: () => staged };
3180
+ }
3181
+ function runStdioSyncServer(deps) {
3182
+ const server = createSyncServer(deps, (msg) => {
3183
+ process.stdout.write(`${JSON.stringify(msg)}
3184
+ `);
3185
+ });
3186
+ let buffer = "";
3187
+ process.stdin.setEncoding("utf8");
3188
+ process.stdin.on("data", (chunk) => {
3189
+ buffer += chunk;
3190
+ let nl = buffer.indexOf("\n");
3191
+ while (nl !== -1) {
3192
+ const line = buffer.slice(0, nl).trim();
3193
+ buffer = buffer.slice(nl + 1);
3194
+ if (line) {
3195
+ try {
3196
+ server.handle(JSON.parse(line));
3197
+ } catch (e) {
3198
+ deps.log?.(`parse error: ${String(e)}`);
3199
+ }
3200
+ }
3201
+ nl = buffer.indexOf("\n");
3202
+ }
3203
+ });
3204
+ }
3205
+
862
3206
  // src/index.ts
863
3207
  var program = new Command();
864
- program.name("aistack").description("Share and clone AI development configurations").version("0.1.0");
3208
+ program.name("aistack").description("Measure and share your AI stack from your terminal").version("0.4.0");
865
3209
  program.command("login").description("Authenticate with AI Stack").action(loginCommand);
866
3210
  program.command("collect").description("Scan and upload AI config files from your project").option("--no-global", "Exclude global config files (~/.claude, etc.)").action((options) => collectCommand({ global: options.global ?? true }));
867
- program.command("create").description("Download and write AI config files from a shared project").argument("<slug>", "Project slug or short ID").action(createCommand);
3211
+ program.command("create").description("Download and write your stack's AI config files").action(createCommand);
3212
+ program.command("mcp").description(
3213
+ "Run the aistack MCP server on stdio (sync preview + gated publish)"
3214
+ ).action(() => {
3215
+ runStdioSyncServer({
3216
+ baseUrl: BASE_URL,
3217
+ log: (line) => process.stderr.write(`[aistack-mcp] ${line}
3218
+ `)
3219
+ });
3220
+ });
3221
+ program.command("sync").description("Scan, preview, and publish measured usage (rolling 30 days)").action(syncCommand);
3222
+ program.command("connect").description("Install the in-session sync surface (MCP server + Skill)").argument("<harness>", 'the harness to connect ("claude")').action(connectCommand);
868
3223
  program.parse();
869
3224
  //# sourceMappingURL=index.js.map