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