bisque-cli 0.2.2
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 +110 -0
- package/dist/index.js +4737 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4737 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../sdk/src/client/workspaces.ts
|
|
13
|
+
var init_workspaces = __esm({
|
|
14
|
+
"../sdk/src/client/workspaces.ts"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// ../sdk/src/errors.ts
|
|
20
|
+
var BisqueApiError, PlanLimitError, PickupModeDeniedError, WorkspaceContextRequiredError, RoleDeniedError, WorkspaceScopeDeniedError, InviteRedeemFailedError;
|
|
21
|
+
var init_errors = __esm({
|
|
22
|
+
"../sdk/src/errors.ts"() {
|
|
23
|
+
"use strict";
|
|
24
|
+
BisqueApiError = class extends Error {
|
|
25
|
+
statusCode;
|
|
26
|
+
constructor(message, statusCode) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "BisqueApiError";
|
|
29
|
+
this.statusCode = statusCode;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
PlanLimitError = class extends BisqueApiError {
|
|
33
|
+
constructor(code, plan, limit, extras) {
|
|
34
|
+
const statusCode = code === "QUOTA_EXCEEDED" ? 429 : 403;
|
|
35
|
+
super(code, statusCode);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.plan = plan;
|
|
38
|
+
this.limit = limit;
|
|
39
|
+
this.extras = extras;
|
|
40
|
+
this.name = "PlanLimitError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
PickupModeDeniedError = class extends BisqueApiError {
|
|
44
|
+
constructor(expected, actual, message = "Task pickup mode does not match claimant class") {
|
|
45
|
+
super(message, 403);
|
|
46
|
+
this.expected = expected;
|
|
47
|
+
this.actual = actual;
|
|
48
|
+
this.name = "PickupModeDeniedError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
WorkspaceContextRequiredError = class extends BisqueApiError {
|
|
52
|
+
reason;
|
|
53
|
+
constructor(message = "X-Bisque-Workspace-Id header is required", reason) {
|
|
54
|
+
super(message, 400);
|
|
55
|
+
this.name = "WorkspaceContextRequiredError";
|
|
56
|
+
this.reason = reason;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
RoleDeniedError = class extends BisqueApiError {
|
|
60
|
+
constructor(expected, actual, message) {
|
|
61
|
+
super(message ?? `Role '${actual}' insufficient; '${expected}' required`, 403);
|
|
62
|
+
this.expected = expected;
|
|
63
|
+
this.actual = actual;
|
|
64
|
+
this.name = "RoleDeniedError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
WorkspaceScopeDeniedError = class extends BisqueApiError {
|
|
68
|
+
constructor(expected, actual, message) {
|
|
69
|
+
super(message ?? `Token scoped to workspace '${expected}'; request targets '${actual}'`, 403);
|
|
70
|
+
this.expected = expected;
|
|
71
|
+
this.actual = actual;
|
|
72
|
+
this.name = "WorkspaceScopeDeniedError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
InviteRedeemFailedError = class extends BisqueApiError {
|
|
76
|
+
constructor(reason, message) {
|
|
77
|
+
super(message ?? `Invite redemption failed: ${reason}`, 409);
|
|
78
|
+
this.reason = reason;
|
|
79
|
+
this.name = "InviteRedeemFailedError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ../sdk/src/client.ts
|
|
86
|
+
function normalizeApiEndpoint(endpoint) {
|
|
87
|
+
return endpoint.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
88
|
+
}
|
|
89
|
+
var BISQUE_PROD_API_ORIGIN;
|
|
90
|
+
var init_client = __esm({
|
|
91
|
+
"../sdk/src/client.ts"() {
|
|
92
|
+
"use strict";
|
|
93
|
+
init_workspaces();
|
|
94
|
+
init_errors();
|
|
95
|
+
init_errors();
|
|
96
|
+
BISQUE_PROD_API_ORIGIN = "https://api.bisquelayer.com";
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ../sdk/src/metadata-kv.ts
|
|
101
|
+
function parseMetadataPair(raw) {
|
|
102
|
+
if (raw === void 0 || raw === null) {
|
|
103
|
+
return { ok: false, error: "Metadata flag value is required (expected k=v)" };
|
|
104
|
+
}
|
|
105
|
+
const eqIdx = raw.indexOf("=");
|
|
106
|
+
if (eqIdx === -1) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: `Invalid --metadata-set value "${raw}": expected k=v (first '=' splits key from value).`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const key = raw.slice(0, eqIdx);
|
|
113
|
+
if (key.length === 0) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
error: `Invalid --metadata-set value "${raw}": key cannot be empty.`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, parsed: { key, value: raw.slice(eqIdx + 1) } };
|
|
120
|
+
}
|
|
121
|
+
function collectMetadataPair(raw, previous) {
|
|
122
|
+
const result = parseMetadataPair(raw);
|
|
123
|
+
if (!result.ok) {
|
|
124
|
+
throw new Error(result.error);
|
|
125
|
+
}
|
|
126
|
+
return { ...previous, [result.parsed.key]: result.parsed.value };
|
|
127
|
+
}
|
|
128
|
+
function collectUnsetKey(raw, previous) {
|
|
129
|
+
if (raw.length === 0) {
|
|
130
|
+
throw new Error("--metadata-unset key cannot be empty");
|
|
131
|
+
}
|
|
132
|
+
return [...previous, raw];
|
|
133
|
+
}
|
|
134
|
+
var init_metadata_kv = __esm({
|
|
135
|
+
"../sdk/src/metadata-kv.ts"() {
|
|
136
|
+
"use strict";
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ../sdk/src/swarm/attribution.ts
|
|
141
|
+
var init_attribution = __esm({
|
|
142
|
+
"../sdk/src/swarm/attribution.ts"() {
|
|
143
|
+
"use strict";
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ../sdk/src/swarm/branch-names.ts
|
|
148
|
+
var init_branch_names = __esm({
|
|
149
|
+
"../sdk/src/swarm/branch-names.ts"() {
|
|
150
|
+
"use strict";
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ../sdk/src/swarm/classifier.ts
|
|
155
|
+
var init_classifier = __esm({
|
|
156
|
+
"../sdk/src/swarm/classifier.ts"() {
|
|
157
|
+
"use strict";
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ../sdk/src/swarm/content-shape.ts
|
|
162
|
+
function escapeLiteral(s) {
|
|
163
|
+
return s.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
164
|
+
}
|
|
165
|
+
function handleDoubleStar(regexSoFar, glob, i) {
|
|
166
|
+
const prevSlash = i > 0 && glob.charAt(i - 1) === "/";
|
|
167
|
+
const nextIsSlash = glob.charAt(i + 2) === "/";
|
|
168
|
+
if (prevSlash && nextIsSlash) {
|
|
169
|
+
return { regex: `${regexSoFar.slice(0, -1)}(?:/.*)?/`, advance: 3 };
|
|
170
|
+
}
|
|
171
|
+
if (nextIsSlash) {
|
|
172
|
+
return { regex: `${regexSoFar}(?:.*/)?`, advance: 3 };
|
|
173
|
+
}
|
|
174
|
+
if (prevSlash) {
|
|
175
|
+
return { regex: `${regexSoFar.slice(0, -1)}(?:/.*)?`, advance: 2 };
|
|
176
|
+
}
|
|
177
|
+
return { regex: `${regexSoFar}.*`, advance: 2 };
|
|
178
|
+
}
|
|
179
|
+
function globToRegex(glob) {
|
|
180
|
+
let regex = "^";
|
|
181
|
+
let i = 0;
|
|
182
|
+
while (i < glob.length) {
|
|
183
|
+
const c = glob.charAt(i);
|
|
184
|
+
if (c !== "*") {
|
|
185
|
+
regex += escapeLiteral(c);
|
|
186
|
+
i += 1;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const isDouble = glob.charAt(i + 1) === "*";
|
|
190
|
+
if (isDouble) {
|
|
191
|
+
const result = handleDoubleStar(regex, glob, i);
|
|
192
|
+
regex = result.regex;
|
|
193
|
+
i += result.advance;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
regex += "[^/]*";
|
|
197
|
+
i += 1;
|
|
198
|
+
}
|
|
199
|
+
regex += "$";
|
|
200
|
+
return new RegExp(regex);
|
|
201
|
+
}
|
|
202
|
+
var FORCING_GLOBS, FORCING_REGEXES;
|
|
203
|
+
var init_content_shape = __esm({
|
|
204
|
+
"../sdk/src/swarm/content-shape.ts"() {
|
|
205
|
+
"use strict";
|
|
206
|
+
FORCING_GLOBS = ["packages/skills/**/*.md", "**/AGENTS.md", "docs/**/*.md", "*/CLAUDE.md"];
|
|
207
|
+
FORCING_REGEXES = FORCING_GLOBS.map((glob) => ({
|
|
208
|
+
glob,
|
|
209
|
+
regex: globToRegex(glob)
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ../sdk/src/swarm/kill-switch.ts
|
|
215
|
+
var init_kill_switch = __esm({
|
|
216
|
+
"../sdk/src/swarm/kill-switch.ts"() {
|
|
217
|
+
"use strict";
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ../sdk/src/swarm/parallel-cap.ts
|
|
222
|
+
var init_parallel_cap = __esm({
|
|
223
|
+
"../sdk/src/swarm/parallel-cap.ts"() {
|
|
224
|
+
"use strict";
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ../sdk/src/swarm/verify-cmd.ts
|
|
229
|
+
var init_verify_cmd = __esm({
|
|
230
|
+
"../sdk/src/swarm/verify-cmd.ts"() {
|
|
231
|
+
"use strict";
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ../sdk/src/swarm/wave-planner.ts
|
|
236
|
+
var init_wave_planner = __esm({
|
|
237
|
+
"../sdk/src/swarm/wave-planner.ts"() {
|
|
238
|
+
"use strict";
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ../sdk/src/swarm/index.ts
|
|
243
|
+
var init_swarm = __esm({
|
|
244
|
+
"../sdk/src/swarm/index.ts"() {
|
|
245
|
+
"use strict";
|
|
246
|
+
init_attribution();
|
|
247
|
+
init_branch_names();
|
|
248
|
+
init_classifier();
|
|
249
|
+
init_content_shape();
|
|
250
|
+
init_kill_switch();
|
|
251
|
+
init_parallel_cap();
|
|
252
|
+
init_verify_cmd();
|
|
253
|
+
init_wave_planner();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ../sdk/src/util/retry.ts
|
|
258
|
+
var DEFAULT_OCC_RETRY_DENYLIST, DEFAULT_OCC_BACKOFF;
|
|
259
|
+
var init_retry = __esm({
|
|
260
|
+
"../sdk/src/util/retry.ts"() {
|
|
261
|
+
"use strict";
|
|
262
|
+
init_errors();
|
|
263
|
+
DEFAULT_OCC_RETRY_DENYLIST = Object.freeze([
|
|
264
|
+
/^POST \/v1\/workspaces\/join$/,
|
|
265
|
+
/^PATCH \/v1\/workspaces\/[^/]+\/members\/[^/]+$/,
|
|
266
|
+
/^DELETE \/v1\/workspaces\/[^/]+\/members\/[^/]+$/,
|
|
267
|
+
/^POST \/v1\/workspaces\/[^/]+\/ownership-transfer$/,
|
|
268
|
+
/^DELETE \/v1\/workspaces\/[^/]+$/
|
|
269
|
+
]);
|
|
270
|
+
DEFAULT_OCC_BACKOFF = Object.freeze({
|
|
271
|
+
initial: 250,
|
|
272
|
+
multiplier: 4,
|
|
273
|
+
jitter: 0.5
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ../sdk/src/util/workspace-header.ts
|
|
279
|
+
function formatWorkspaceHeader(input) {
|
|
280
|
+
const annotation = input.type === "personal" ? `${input.role}, personal` : input.role;
|
|
281
|
+
return `[workspace: ${input.name} (${annotation})]`;
|
|
282
|
+
}
|
|
283
|
+
var init_workspace_header = __esm({
|
|
284
|
+
"../sdk/src/util/workspace-header.ts"() {
|
|
285
|
+
"use strict";
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ../sdk/src/index.ts
|
|
290
|
+
var init_src = __esm({
|
|
291
|
+
"../sdk/src/index.ts"() {
|
|
292
|
+
"use strict";
|
|
293
|
+
init_workspaces();
|
|
294
|
+
init_client();
|
|
295
|
+
init_errors();
|
|
296
|
+
init_metadata_kv();
|
|
297
|
+
init_swarm();
|
|
298
|
+
init_retry();
|
|
299
|
+
init_workspace_header();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// src/config.ts
|
|
304
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
305
|
+
import { homedir } from "node:os";
|
|
306
|
+
import { join } from "node:path";
|
|
307
|
+
var DEFAULT_CONFIG, WORKSPACES_CACHE_TTL_MS, Config;
|
|
308
|
+
var init_config = __esm({
|
|
309
|
+
"src/config.ts"() {
|
|
310
|
+
"use strict";
|
|
311
|
+
init_src();
|
|
312
|
+
DEFAULT_CONFIG = {
|
|
313
|
+
apiEndpoint: BISQUE_PROD_API_ORIGIN,
|
|
314
|
+
webAppUrl: "https://bisquelayer.com",
|
|
315
|
+
environment: "prod"
|
|
316
|
+
};
|
|
317
|
+
WORKSPACES_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
318
|
+
Config = class {
|
|
319
|
+
configDir;
|
|
320
|
+
configPath;
|
|
321
|
+
constructor(configDir) {
|
|
322
|
+
this.configDir = configDir ?? join(homedir(), ".bisque");
|
|
323
|
+
this.configPath = join(this.configDir, "config.json");
|
|
324
|
+
}
|
|
325
|
+
async get(key) {
|
|
326
|
+
if (key === "apiEndpoint" && process.env["BISQUE_API_ENDPOINT"]) {
|
|
327
|
+
return normalizeApiEndpoint(process.env["BISQUE_API_ENDPOINT"]);
|
|
328
|
+
}
|
|
329
|
+
if (key === "webAppUrl" && process.env["BISQUE_WEB_APP_URL"]) {
|
|
330
|
+
return process.env["BISQUE_WEB_APP_URL"];
|
|
331
|
+
}
|
|
332
|
+
if (key === "authToken" && process.env["BISQUE_AUTH_TOKEN"]) {
|
|
333
|
+
return process.env["BISQUE_AUTH_TOKEN"];
|
|
334
|
+
}
|
|
335
|
+
const config = await this.load();
|
|
336
|
+
if (key === "apiEndpoint") {
|
|
337
|
+
return normalizeApiEndpoint(config.apiEndpoint);
|
|
338
|
+
}
|
|
339
|
+
return config[key];
|
|
340
|
+
}
|
|
341
|
+
async set(key, value) {
|
|
342
|
+
const config = await this.load();
|
|
343
|
+
if (key === "apiEndpoint" && typeof value === "string") {
|
|
344
|
+
config[key] = normalizeApiEndpoint(value);
|
|
345
|
+
} else {
|
|
346
|
+
config[key] = value;
|
|
347
|
+
}
|
|
348
|
+
await this.save(config);
|
|
349
|
+
}
|
|
350
|
+
async getAll() {
|
|
351
|
+
return this.load();
|
|
352
|
+
}
|
|
353
|
+
async load() {
|
|
354
|
+
try {
|
|
355
|
+
const raw = await readFile(this.configPath, "utf-8");
|
|
356
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
357
|
+
} catch {
|
|
358
|
+
return { ...DEFAULT_CONFIG };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async save(config) {
|
|
362
|
+
await mkdir(this.configDir, { recursive: true, mode: 448 });
|
|
363
|
+
await writeFile(this.configPath, JSON.stringify(config, null, 2), {
|
|
364
|
+
encoding: "utf-8",
|
|
365
|
+
mode: 384
|
|
366
|
+
});
|
|
367
|
+
if (process.platform !== "win32") {
|
|
368
|
+
await chmod(this.configPath, 384);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// src/utils/runtime-context.ts
|
|
376
|
+
function setRuntimeContext(ctx) {
|
|
377
|
+
runtime = ctx;
|
|
378
|
+
}
|
|
379
|
+
function getCurrentWorkspaceContext() {
|
|
380
|
+
return runtime.workspaceContext;
|
|
381
|
+
}
|
|
382
|
+
function getCurrentWorkspaceId() {
|
|
383
|
+
return runtime.workspaceContext?.workspaceId;
|
|
384
|
+
}
|
|
385
|
+
function getHeaderPreferences() {
|
|
386
|
+
return { enabled: runtime.headerEnabled, quiet: runtime.quiet };
|
|
387
|
+
}
|
|
388
|
+
function getRuntimeConfig() {
|
|
389
|
+
return runtime.config;
|
|
390
|
+
}
|
|
391
|
+
var runtime;
|
|
392
|
+
var init_runtime_context = __esm({
|
|
393
|
+
"src/utils/runtime-context.ts"() {
|
|
394
|
+
"use strict";
|
|
395
|
+
runtime = { headerEnabled: true, quiet: false };
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// src/utils/workspace-context.ts
|
|
400
|
+
var workspace_context_exports = {};
|
|
401
|
+
__export(workspace_context_exports, {
|
|
402
|
+
__resetStorageAttributionForTests: () => __resetStorageAttributionForTests,
|
|
403
|
+
formatWorkspaceHeader: () => formatWorkspaceHeader,
|
|
404
|
+
getSelfUserId: () => getSelfUserId,
|
|
405
|
+
getWorkspacesCache: () => getWorkspacesCache,
|
|
406
|
+
maybeEmitFirstWriteNote: () => maybeEmitFirstWriteNote,
|
|
407
|
+
printWorkspaceHeader: () => printWorkspaceHeader,
|
|
408
|
+
refreshWorkspacesCache: () => refreshWorkspacesCache,
|
|
409
|
+
resolveWorkspaceContext: () => resolveWorkspaceContext,
|
|
410
|
+
resolveWorkspaceId: () => resolveWorkspaceId
|
|
411
|
+
});
|
|
412
|
+
function __resetStorageAttributionForTests() {
|
|
413
|
+
storageAttributionFiredThisProcess = false;
|
|
414
|
+
}
|
|
415
|
+
function resolveWorkspaceId(flagValue) {
|
|
416
|
+
if (flagValue !== void 0 && flagValue !== "") {
|
|
417
|
+
return { workspaceId: flagValue, source: "flag" };
|
|
418
|
+
}
|
|
419
|
+
const env = process.env["BISQUE_WORKSPACE_ID"];
|
|
420
|
+
if (env !== void 0 && env !== "") {
|
|
421
|
+
return { workspaceId: env, source: "env" };
|
|
422
|
+
}
|
|
423
|
+
return { workspaceId: void 0, source: "default" };
|
|
424
|
+
}
|
|
425
|
+
function findInCache(cache, workspaceId) {
|
|
426
|
+
if (!cache) return void 0;
|
|
427
|
+
return cache.workspaces.find((w) => w.workspaceId === workspaceId);
|
|
428
|
+
}
|
|
429
|
+
function isCacheFresh(cachedAt) {
|
|
430
|
+
if (!cachedAt) return false;
|
|
431
|
+
const cachedMs = Date.parse(cachedAt);
|
|
432
|
+
if (Number.isNaN(cachedMs)) return false;
|
|
433
|
+
return Date.now() - cachedMs < WORKSPACES_CACHE_TTL_MS;
|
|
434
|
+
}
|
|
435
|
+
async function refreshWorkspacesCache(config) {
|
|
436
|
+
const client = new ApiClient(config);
|
|
437
|
+
const response = await client.get("/v1/me/workspaces");
|
|
438
|
+
await config.set("workspacesCache", response);
|
|
439
|
+
await config.set("activeWorkspaceCachedAt", (/* @__PURE__ */ new Date()).toISOString());
|
|
440
|
+
return response;
|
|
441
|
+
}
|
|
442
|
+
async function getWorkspacesCache(config) {
|
|
443
|
+
const token = await config.get("authToken");
|
|
444
|
+
if (!token) return void 0;
|
|
445
|
+
const all = await config.getAll();
|
|
446
|
+
if (all.workspacesCache && isCacheFresh(all.activeWorkspaceCachedAt)) {
|
|
447
|
+
return all.workspacesCache;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
return await refreshWorkspacesCache(config);
|
|
451
|
+
} catch {
|
|
452
|
+
return all.workspacesCache;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function findPersonalDefault(cache) {
|
|
456
|
+
if (!cache) return void 0;
|
|
457
|
+
return cache.workspaces.find((w) => w.isPersonal);
|
|
458
|
+
}
|
|
459
|
+
async function resolveWorkspaceContext(config, flagValue) {
|
|
460
|
+
const { workspaceId: explicit, source: explicitSource } = resolveWorkspaceId(flagValue);
|
|
461
|
+
const cache = await getWorkspacesCache(config);
|
|
462
|
+
if (explicit) {
|
|
463
|
+
const item = findInCache(cache, explicit);
|
|
464
|
+
if (item) {
|
|
465
|
+
return {
|
|
466
|
+
workspaceId: item.workspaceId,
|
|
467
|
+
name: item.name,
|
|
468
|
+
role: item.role,
|
|
469
|
+
type: item.isPersonal ? "personal" : "shared",
|
|
470
|
+
source: explicitSource
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
workspaceId: explicit,
|
|
475
|
+
name: explicit,
|
|
476
|
+
role: "viewer",
|
|
477
|
+
type: "shared",
|
|
478
|
+
source: explicitSource
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const configActive = await config.get("activeWorkspaceId");
|
|
482
|
+
if (configActive) {
|
|
483
|
+
const item = findInCache(cache, configActive);
|
|
484
|
+
if (item) {
|
|
485
|
+
return {
|
|
486
|
+
workspaceId: item.workspaceId,
|
|
487
|
+
name: item.name,
|
|
488
|
+
role: item.role,
|
|
489
|
+
type: item.isPersonal ? "personal" : "shared",
|
|
490
|
+
source: "config"
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const personal = findPersonalDefault(cache);
|
|
495
|
+
if (personal) {
|
|
496
|
+
return {
|
|
497
|
+
workspaceId: personal.workspaceId,
|
|
498
|
+
name: personal.name,
|
|
499
|
+
role: personal.role,
|
|
500
|
+
type: "personal",
|
|
501
|
+
source: "default"
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
return void 0;
|
|
505
|
+
}
|
|
506
|
+
function printWorkspaceHeader(ctx, options = {}) {
|
|
507
|
+
if (!ctx) return;
|
|
508
|
+
if (options.quiet) return;
|
|
509
|
+
const header = formatWorkspaceHeader({ name: ctx.name, role: ctx.role, type: ctx.type });
|
|
510
|
+
if (options.enabled === false) {
|
|
511
|
+
process.stderr.write(`${header}
|
|
512
|
+
`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
console.log(header);
|
|
516
|
+
}
|
|
517
|
+
async function fetchStorageUsage(config) {
|
|
518
|
+
try {
|
|
519
|
+
const client = new ApiClient(config);
|
|
520
|
+
const me = await client.get("/v1/auth/me");
|
|
521
|
+
const used = me.usage?.storageMb;
|
|
522
|
+
const limit = me.limits?.storageMb;
|
|
523
|
+
if (typeof used !== "number" || typeof limit !== "number") return void 0;
|
|
524
|
+
return { used, limit };
|
|
525
|
+
} catch {
|
|
526
|
+
return void 0;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function maybeEmitFirstWriteNote(ctx, options, config) {
|
|
530
|
+
if (storageAttributionFiredThisProcess) return;
|
|
531
|
+
if (!ctx) return;
|
|
532
|
+
if (ctx.type !== "shared") return;
|
|
533
|
+
if (options.quiet) return;
|
|
534
|
+
if (options.json) return;
|
|
535
|
+
storageAttributionFiredThisProcess = true;
|
|
536
|
+
const header = formatWorkspaceHeader({ name: ctx.name, role: ctx.role, type: ctx.type });
|
|
537
|
+
const usage = await fetchStorageUsage(config);
|
|
538
|
+
const usageClause = usage ? ` (${usage.used} / ${usage.limit} MB used)` : "";
|
|
539
|
+
process.stderr.write(
|
|
540
|
+
`${header} Writing to shared workspace "${ctx.name}" \u2014 counts against your personal storage quota${usageClause}.
|
|
541
|
+
`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
async function getSelfUserId(config) {
|
|
545
|
+
try {
|
|
546
|
+
const client = new ApiClient(config);
|
|
547
|
+
const me = await client.get("/v1/auth/me");
|
|
548
|
+
return me.userId;
|
|
549
|
+
} catch {
|
|
550
|
+
return void 0;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
var storageAttributionFiredThisProcess;
|
|
554
|
+
var init_workspace_context = __esm({
|
|
555
|
+
"src/utils/workspace-context.ts"() {
|
|
556
|
+
"use strict";
|
|
557
|
+
init_src();
|
|
558
|
+
init_config();
|
|
559
|
+
init_api_client();
|
|
560
|
+
init_src();
|
|
561
|
+
storageAttributionFiredThisProcess = false;
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// src/utils/api-client.ts
|
|
566
|
+
var api_client_exports = {};
|
|
567
|
+
__export(api_client_exports, {
|
|
568
|
+
ApiClient: () => ApiClient,
|
|
569
|
+
__resetMembershipMutationFlagForTests: () => __resetMembershipMutationFlagForTests,
|
|
570
|
+
consumeMembershipMutationFlag: () => consumeMembershipMutationFlag
|
|
571
|
+
});
|
|
572
|
+
function consumeMembershipMutationFlag() {
|
|
573
|
+
const flagged = membershipMutatedThisProcess;
|
|
574
|
+
membershipMutatedThisProcess = false;
|
|
575
|
+
return flagged;
|
|
576
|
+
}
|
|
577
|
+
function __resetMembershipMutationFlagForTests() {
|
|
578
|
+
membershipMutatedThisProcess = false;
|
|
579
|
+
}
|
|
580
|
+
var membershipMutatedThisProcess, ApiClient;
|
|
581
|
+
var init_api_client = __esm({
|
|
582
|
+
"src/utils/api-client.ts"() {
|
|
583
|
+
"use strict";
|
|
584
|
+
init_config();
|
|
585
|
+
init_runtime_context();
|
|
586
|
+
membershipMutatedThisProcess = false;
|
|
587
|
+
ApiClient = class {
|
|
588
|
+
config;
|
|
589
|
+
maxRetries = 3;
|
|
590
|
+
retryDelays = [100, 200, 400];
|
|
591
|
+
// Exponential backoff in ms
|
|
592
|
+
workspaceId;
|
|
593
|
+
constructor(config, options) {
|
|
594
|
+
this.config = config ?? new Config();
|
|
595
|
+
this.workspaceId = options?.workspaceId ?? getCurrentWorkspaceId();
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Build the per-request header bag. Extracted from `fetch()` to keep
|
|
599
|
+
* the request loop body under Biome's cognitive-complexity ceiling.
|
|
600
|
+
*/
|
|
601
|
+
buildHeaders(options, authToken) {
|
|
602
|
+
const hasBody = options?.body !== void 0 && options.body !== null;
|
|
603
|
+
const headers = {};
|
|
604
|
+
if (hasBody) headers["Content-Type"] = "application/json";
|
|
605
|
+
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
606
|
+
if (this.workspaceId !== void 0 && this.workspaceId !== "") {
|
|
607
|
+
headers["X-Bisque-Workspace-Id"] = this.workspaceId;
|
|
608
|
+
}
|
|
609
|
+
return headers;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Inspect the response headers for the `X-Workspace-Membership-Mutated`
|
|
613
|
+
* signal and trip the per-process flag. Defensive on `response.headers`
|
|
614
|
+
* because test mocks may not surface a Headers-shaped object.
|
|
615
|
+
*/
|
|
616
|
+
observeMembershipMutationHeader(response) {
|
|
617
|
+
if (!response.headers || typeof response.headers.get !== "function") return;
|
|
618
|
+
const mutatedHeader = response.headers.get("x-workspace-membership-mutated");
|
|
619
|
+
if (mutatedHeader === "true") {
|
|
620
|
+
membershipMutatedThisProcess = true;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async fetch(path2, options) {
|
|
624
|
+
const apiEndpoint = await this.config.get("apiEndpoint");
|
|
625
|
+
const authToken = await this.config.get("authToken");
|
|
626
|
+
const url = `${apiEndpoint}${path2}`;
|
|
627
|
+
const headers = this.buildHeaders(options, authToken);
|
|
628
|
+
let lastError = null;
|
|
629
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
630
|
+
try {
|
|
631
|
+
const response = await globalThis.fetch(url, {
|
|
632
|
+
...options,
|
|
633
|
+
headers: {
|
|
634
|
+
...headers,
|
|
635
|
+
...options?.headers
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
this.observeMembershipMutationHeader(response);
|
|
639
|
+
if (response.status === 401) {
|
|
640
|
+
throw new Error(
|
|
641
|
+
"Authentication failed. Your session may have expired. Please login again."
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
if (response.status === 500 || response.status === 503) {
|
|
645
|
+
if (attempt < this.maxRetries - 1) {
|
|
646
|
+
console.error(
|
|
647
|
+
`Server error (${response.status}), retrying... (attempt ${attempt + 1}/${this.maxRetries})`
|
|
648
|
+
);
|
|
649
|
+
await this.delay(this.retryDelays[attempt] ?? 400);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const errorBody = await response.text();
|
|
653
|
+
throw new Error(`Server error after ${this.maxRetries} attempts: ${errorBody}`);
|
|
654
|
+
}
|
|
655
|
+
if (!response.ok) {
|
|
656
|
+
const errorBody = await response.text();
|
|
657
|
+
throw new Error(`API request failed (${response.status}): ${errorBody}`);
|
|
658
|
+
}
|
|
659
|
+
return response;
|
|
660
|
+
} catch (error) {
|
|
661
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
662
|
+
if (lastError.message.includes("Authentication failed")) {
|
|
663
|
+
throw lastError;
|
|
664
|
+
}
|
|
665
|
+
if (this.isNetworkError(error)) {
|
|
666
|
+
if (attempt < this.maxRetries - 1) {
|
|
667
|
+
console.error(`Network error, retrying... (attempt ${attempt + 1}/${this.maxRetries})`);
|
|
668
|
+
await this.delay(this.retryDelays[attempt] ?? 400);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const message = this.getNetworkErrorMessage(apiEndpoint, error);
|
|
672
|
+
throw new Error(message);
|
|
673
|
+
}
|
|
674
|
+
throw lastError;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
throw lastError ?? new Error("Request failed after all retries");
|
|
678
|
+
}
|
|
679
|
+
isNetworkError(error) {
|
|
680
|
+
if (!(error instanceof Error)) return false;
|
|
681
|
+
const message = error.message.toLowerCase();
|
|
682
|
+
return message.includes("fetch failed") || message.includes("network") || message.includes("econnrefused") || message.includes("timeout") || message.includes("enotfound");
|
|
683
|
+
}
|
|
684
|
+
getNetworkErrorMessage(apiEndpoint, error) {
|
|
685
|
+
if (!(error instanceof Error)) return `Network error: ${String(error)}`;
|
|
686
|
+
const message = error.message.toLowerCase();
|
|
687
|
+
if (message.includes("econnrefused")) {
|
|
688
|
+
return `Cannot connect to API server at ${apiEndpoint}`;
|
|
689
|
+
}
|
|
690
|
+
if (message.includes("timeout")) {
|
|
691
|
+
return `Request timed out after 30 seconds`;
|
|
692
|
+
}
|
|
693
|
+
if (message.includes("enotfound")) {
|
|
694
|
+
return `Cannot resolve hostname: ${apiEndpoint}`;
|
|
695
|
+
}
|
|
696
|
+
return `Network error: ${error.message}`;
|
|
697
|
+
}
|
|
698
|
+
delay(ms) {
|
|
699
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
700
|
+
}
|
|
701
|
+
async get(path2) {
|
|
702
|
+
const response = await this.fetch(path2, { method: "GET" });
|
|
703
|
+
return response.json();
|
|
704
|
+
}
|
|
705
|
+
async post(path2, body) {
|
|
706
|
+
await this.maybeEmitFirstWriteNote();
|
|
707
|
+
const response = await this.fetch(path2, {
|
|
708
|
+
method: "POST",
|
|
709
|
+
body: JSON.stringify(body)
|
|
710
|
+
});
|
|
711
|
+
if (response.status === 204) {
|
|
712
|
+
return void 0;
|
|
713
|
+
}
|
|
714
|
+
return response.json();
|
|
715
|
+
}
|
|
716
|
+
async put(path2, body) {
|
|
717
|
+
await this.maybeEmitFirstWriteNote();
|
|
718
|
+
const response = await this.fetch(path2, {
|
|
719
|
+
method: "PUT",
|
|
720
|
+
body: JSON.stringify(body)
|
|
721
|
+
});
|
|
722
|
+
return response.json();
|
|
723
|
+
}
|
|
724
|
+
async patch(path2, body) {
|
|
725
|
+
await this.maybeEmitFirstWriteNote();
|
|
726
|
+
const response = await this.fetch(path2, {
|
|
727
|
+
method: "PATCH",
|
|
728
|
+
body: JSON.stringify(body)
|
|
729
|
+
});
|
|
730
|
+
return response.json();
|
|
731
|
+
}
|
|
732
|
+
async delete(path2) {
|
|
733
|
+
await this.maybeEmitFirstWriteNote();
|
|
734
|
+
const response = await this.fetch(path2, { method: "DELETE" });
|
|
735
|
+
if (response.status === 204) {
|
|
736
|
+
return void 0;
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
return await response.json();
|
|
740
|
+
} catch {
|
|
741
|
+
return void 0;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Fire the per-process first-write storage-attribution note when the
|
|
746
|
+
* resolved workspace is shared. Idempotent across all `ApiClient`
|
|
747
|
+
* instances within a process. Lazily imported to avoid an early
|
|
748
|
+
* circular dependency (workspace-context.ts imports ApiClient too).
|
|
749
|
+
*
|
|
750
|
+
* Suppressed when:
|
|
751
|
+
* - The resolved workspace is personal (writes don't cross quotas).
|
|
752
|
+
* - `--quiet` is set (caller wants pristine output).
|
|
753
|
+
* - The caller is in JSON mode (the note would corrupt JSON parsing
|
|
754
|
+
* — detected via the `--json` heuristic on the header preference;
|
|
755
|
+
* the runtime-context singleton doesn't surface json-mode today,
|
|
756
|
+
* so callers that pipe `--json` should also pass `--quiet`).
|
|
757
|
+
*
|
|
758
|
+
* Per-process flag is owned by `workspace-context.ts`'s
|
|
759
|
+
* `maybeEmitFirstWriteNote`.
|
|
760
|
+
*/
|
|
761
|
+
async maybeEmitFirstWriteNote() {
|
|
762
|
+
const ctx = getCurrentWorkspaceContext();
|
|
763
|
+
if (!ctx) return;
|
|
764
|
+
if (ctx.type !== "shared") return;
|
|
765
|
+
const { quiet } = getHeaderPreferences();
|
|
766
|
+
const config = getRuntimeConfig() ?? this.config;
|
|
767
|
+
const { maybeEmitFirstWriteNote: maybeEmitFirstWriteNote2 } = await Promise.resolve().then(() => (init_workspace_context(), workspace_context_exports));
|
|
768
|
+
await maybeEmitFirstWriteNote2(ctx, { quiet }, config);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// src/index.ts
|
|
775
|
+
import { createRequire } from "node:module";
|
|
776
|
+
import { Command, Option as Option3 } from "commander";
|
|
777
|
+
|
|
778
|
+
// src/commands/add.ts
|
|
779
|
+
init_api_client();
|
|
780
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
781
|
+
import { basename } from "node:path";
|
|
782
|
+
function slugify(name) {
|
|
783
|
+
return name.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-_]/g, "");
|
|
784
|
+
}
|
|
785
|
+
function deriveFilepath(filename, options) {
|
|
786
|
+
if (options.project) {
|
|
787
|
+
return `1-Projects/${slugify(options.project)}/${filename}`;
|
|
788
|
+
}
|
|
789
|
+
if (options.area) {
|
|
790
|
+
return `2-Areas/${slugify(options.area)}/${filename}`;
|
|
791
|
+
}
|
|
792
|
+
if (options.resource) {
|
|
793
|
+
return `3-Resources/${slugify(options.resource)}/${filename}`;
|
|
794
|
+
}
|
|
795
|
+
return `0-Inbox/${filename}`;
|
|
796
|
+
}
|
|
797
|
+
function categoryFromPath(filepath) {
|
|
798
|
+
if (filepath.startsWith("0-Inbox/")) return "inbox";
|
|
799
|
+
if (filepath.startsWith("1-Projects/")) return "project";
|
|
800
|
+
if (filepath.startsWith("2-Areas/")) return "area";
|
|
801
|
+
if (filepath.startsWith("3-Resources/")) return "resource";
|
|
802
|
+
if (filepath.startsWith("4-Archive/")) return "archive";
|
|
803
|
+
return "inbox";
|
|
804
|
+
}
|
|
805
|
+
function parseMetadataJson(raw) {
|
|
806
|
+
try {
|
|
807
|
+
const parsed = JSON.parse(raw);
|
|
808
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
809
|
+
return { error: "Error: --metadata must be a JSON object" };
|
|
810
|
+
}
|
|
811
|
+
return { value: parsed };
|
|
812
|
+
} catch {
|
|
813
|
+
return { error: "Error: --metadata must be valid JSON" };
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async function resolveContent(file, options) {
|
|
817
|
+
if (options.content) {
|
|
818
|
+
if (!options.title) {
|
|
819
|
+
return { error: "Error: --title is required when using --content" };
|
|
820
|
+
}
|
|
821
|
+
return { title: options.title, content: options.content };
|
|
822
|
+
}
|
|
823
|
+
if (file) {
|
|
824
|
+
const content = await readFile2(file, "utf-8");
|
|
825
|
+
return { title: options.title ?? basename(file), content };
|
|
826
|
+
}
|
|
827
|
+
return { error: "Error: provide a file path or use --content with --title" };
|
|
828
|
+
}
|
|
829
|
+
function parseDocIdList(val) {
|
|
830
|
+
return val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
831
|
+
}
|
|
832
|
+
function buildCreateBody(resolved, filepath, category, options, metadata) {
|
|
833
|
+
return {
|
|
834
|
+
title: resolved.title,
|
|
835
|
+
filepath,
|
|
836
|
+
category,
|
|
837
|
+
tags: options.tags ?? [],
|
|
838
|
+
content: resolved.content,
|
|
839
|
+
...metadata && { metadata },
|
|
840
|
+
// Freshness & Staleness v1 (T3). Pass through verbatim; backend
|
|
841
|
+
// enum gates `status`, supersedes array is accepted on create.
|
|
842
|
+
...options.status !== void 0 && { status: options.status },
|
|
843
|
+
...options.supersedes !== void 0 && { supersedes: options.supersedes }
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function registerAddCommand(program2) {
|
|
847
|
+
program2.command("add [file]").description("Add a document from a local file or inline content").option("-p, --project <name>", "Place document under a PARA project").option("-a, --area <name>", "Place document under a PARA area").option("-r, --resource <name>", "Place document under a PARA resource").option(
|
|
848
|
+
"-t, --tags <tags>",
|
|
849
|
+
"Comma-separated tags",
|
|
850
|
+
(val) => val.split(",").map((t) => t.trim())
|
|
851
|
+
).option("--filepath <path>", "Explicit PARA filepath (overrides --project/--area/--resource)").option("--content <text>", "Inline content (skip file reading)").option("--title <name>", "Document title (required with --content)").option("--metadata <json>", "JSON metadata object").option("--status <status>", "Lifecycle state: live | superseded | archived. Defaults to live.").option(
|
|
852
|
+
"--supersedes <docIds>",
|
|
853
|
+
"Comma-separated doc UUIDs this doc supersedes. Optional on create.",
|
|
854
|
+
parseDocIdList
|
|
855
|
+
).action(async (file, options) => {
|
|
856
|
+
try {
|
|
857
|
+
const resolved = await resolveContent(file, options);
|
|
858
|
+
if ("error" in resolved) {
|
|
859
|
+
console.error(resolved.error);
|
|
860
|
+
process.exitCode = 1;
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
let metadata;
|
|
864
|
+
if (options.metadata) {
|
|
865
|
+
const result = parseMetadataJson(options.metadata);
|
|
866
|
+
if ("error" in result) {
|
|
867
|
+
console.error(result.error);
|
|
868
|
+
process.exitCode = 1;
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
metadata = result.value;
|
|
872
|
+
}
|
|
873
|
+
const filepath = options.filepath ?? deriveFilepath(resolved.title, options);
|
|
874
|
+
const category = categoryFromPath(filepath);
|
|
875
|
+
const client = new ApiClient();
|
|
876
|
+
const createResult = await client.post(
|
|
877
|
+
"/v1/documents",
|
|
878
|
+
buildCreateBody(resolved, filepath, category, options, metadata)
|
|
879
|
+
);
|
|
880
|
+
console.log(`Document created: ${createResult.documentId}`);
|
|
881
|
+
console.log(` Path: ${filepath}`);
|
|
882
|
+
console.log(` Category: ${category}`);
|
|
883
|
+
} catch (error) {
|
|
884
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
885
|
+
console.error(`Error: ${message}`);
|
|
886
|
+
process.exitCode = 1;
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/agents-md/sections.ts
|
|
892
|
+
var KEYWORDS_SENTINEL = "<!-- bisque-keywords:mechanical -->";
|
|
893
|
+
var CONTENT_EXTRACT_TARGET = 280;
|
|
894
|
+
var CONTENT_EXTRACT_HARD_MAX = 400;
|
|
895
|
+
function normalizeDir(dirPath) {
|
|
896
|
+
return dirPath.endsWith("/") ? dirPath : `${dirPath}/`;
|
|
897
|
+
}
|
|
898
|
+
function basename2(filepath) {
|
|
899
|
+
const parts = filepath.split("/");
|
|
900
|
+
return parts[parts.length - 1] ?? filepath;
|
|
901
|
+
}
|
|
902
|
+
function stripFrontmatter(body) {
|
|
903
|
+
if (!body.startsWith("---\n")) return body;
|
|
904
|
+
const end = body.indexOf("\n---", 4);
|
|
905
|
+
return end === -1 ? body : body.slice(end + 4);
|
|
906
|
+
}
|
|
907
|
+
function findFirstParagraphLines(body) {
|
|
908
|
+
const paragraph = [];
|
|
909
|
+
let inCodeBlock = false;
|
|
910
|
+
let started = false;
|
|
911
|
+
for (const rawLine of body.split("\n")) {
|
|
912
|
+
const line = rawLine.trimEnd();
|
|
913
|
+
if (line.startsWith("```") || line.startsWith("~~~")) {
|
|
914
|
+
inCodeBlock = !inCodeBlock;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (inCodeBlock) continue;
|
|
918
|
+
if (line.startsWith("#")) {
|
|
919
|
+
if (started) break;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (line.trim() === "") {
|
|
923
|
+
if (started) break;
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
paragraph.push(line.trim());
|
|
927
|
+
started = true;
|
|
928
|
+
}
|
|
929
|
+
return paragraph;
|
|
930
|
+
}
|
|
931
|
+
function truncateAtSentenceBoundary(text) {
|
|
932
|
+
if (text.length <= CONTENT_EXTRACT_TARGET) return text;
|
|
933
|
+
const window = text.slice(0, CONTENT_EXTRACT_HARD_MAX);
|
|
934
|
+
let bestIdx = -1;
|
|
935
|
+
for (const ch of [".", "!", "?"]) {
|
|
936
|
+
const idx = window.lastIndexOf(`${ch} `, CONTENT_EXTRACT_TARGET);
|
|
937
|
+
if (idx > bestIdx) bestIdx = idx;
|
|
938
|
+
}
|
|
939
|
+
if (bestIdx > CONTENT_EXTRACT_TARGET / 2) {
|
|
940
|
+
return `${text.slice(0, bestIdx + 1)} \u2026`;
|
|
941
|
+
}
|
|
942
|
+
return `${text.slice(0, CONTENT_EXTRACT_TARGET).trim()}\u2026`;
|
|
943
|
+
}
|
|
944
|
+
function extractFirstParagraph(content) {
|
|
945
|
+
if (!content) return void 0;
|
|
946
|
+
const body = stripFrontmatter(content);
|
|
947
|
+
const lines = findFirstParagraphLines(body);
|
|
948
|
+
if (lines.length === 0) return void 0;
|
|
949
|
+
const text = lines.join(" ").replace(/\s+/g, " ").trim();
|
|
950
|
+
if (text.length === 0) return void 0;
|
|
951
|
+
return truncateAtSentenceBoundary(text);
|
|
952
|
+
}
|
|
953
|
+
function extractPurposeLine(content) {
|
|
954
|
+
if (!content) return void 0;
|
|
955
|
+
const match = content.match(/^\*\*Purpose:\*\*\s+(.+)$/m);
|
|
956
|
+
if (!match) return void 0;
|
|
957
|
+
const purpose = match[1]?.trim();
|
|
958
|
+
if (!purpose) return void 0;
|
|
959
|
+
if (purpose === "_(describe this directory)_") return void 0;
|
|
960
|
+
return purpose;
|
|
961
|
+
}
|
|
962
|
+
function renderFileOutline(name, doc) {
|
|
963
|
+
const summary = doc.summary?.trim();
|
|
964
|
+
const body = summary && summary.length > 0 ? summary : extractFirstParagraph(doc.content) ?? "_(no description)_";
|
|
965
|
+
return `### ${name}
|
|
966
|
+
${body}`;
|
|
967
|
+
}
|
|
968
|
+
function renderSubdirOutline(dir, subdirName, docs) {
|
|
969
|
+
const subdir = `${dir}${subdirName}/`;
|
|
970
|
+
const childAgents = docs.find((d) => d.filepath === `${subdir}AGENTS.md`);
|
|
971
|
+
const purpose = extractPurposeLine(childAgents?.content);
|
|
972
|
+
const childFiles = [];
|
|
973
|
+
for (const d of docs) {
|
|
974
|
+
if (!d.filepath.startsWith(subdir)) continue;
|
|
975
|
+
const rest = d.filepath.slice(subdir.length);
|
|
976
|
+
if (rest.includes("/")) continue;
|
|
977
|
+
if (rest === "AGENTS.md") continue;
|
|
978
|
+
childFiles.push(rest);
|
|
979
|
+
}
|
|
980
|
+
childFiles.sort();
|
|
981
|
+
const headLine = `### ${subdirName}/`;
|
|
982
|
+
const purposeLine = purpose ?? "_(no purpose set)_";
|
|
983
|
+
const contentsBlock = childFiles.length === 0 ? "Contents: _(none)_" : `Contents:
|
|
984
|
+
${childFiles.map((f) => `- ${f}`).join("\n")}`;
|
|
985
|
+
return `${headLine}
|
|
986
|
+
${purposeLine}
|
|
987
|
+
|
|
988
|
+
${contentsBlock}`;
|
|
989
|
+
}
|
|
990
|
+
function generateWhatsHereSection(dirPath, docs) {
|
|
991
|
+
const dir = normalizeDir(dirPath);
|
|
992
|
+
const inDir = docs.filter((d) => {
|
|
993
|
+
if (!d.filepath.startsWith(dir)) return false;
|
|
994
|
+
const rest = d.filepath.slice(dir.length);
|
|
995
|
+
if (rest.includes("/")) return false;
|
|
996
|
+
if (rest === "AGENTS.md") return false;
|
|
997
|
+
return true;
|
|
998
|
+
});
|
|
999
|
+
if (inDir.length === 0) {
|
|
1000
|
+
return "## What's here\n_(empty)_";
|
|
1001
|
+
}
|
|
1002
|
+
inDir.sort((a, b) => basename2(a.filepath).localeCompare(basename2(b.filepath)));
|
|
1003
|
+
const blocks = inDir.map((d) => renderFileOutline(basename2(d.filepath), d));
|
|
1004
|
+
return `## What's here
|
|
1005
|
+
${blocks.join("\n\n")}`;
|
|
1006
|
+
}
|
|
1007
|
+
function generateHeader(dirPath) {
|
|
1008
|
+
const trimmed = dirPath.endsWith("/") ? dirPath.slice(0, -1) : dirPath;
|
|
1009
|
+
const parts = trimmed.split("/").filter(Boolean);
|
|
1010
|
+
const title = parts[parts.length - 1] ?? trimmed;
|
|
1011
|
+
const parent = parts.length <= 1 ? "_(workspace root)_" : `${parts.slice(0, -1).join("/")}/`;
|
|
1012
|
+
return `# ${title}
|
|
1013
|
+
|
|
1014
|
+
**Purpose:** _(describe this directory)_
|
|
1015
|
+
**Parent:** ${parent}`;
|
|
1016
|
+
}
|
|
1017
|
+
function generateSubdirectoriesSection(dirPath, docs) {
|
|
1018
|
+
const dir = normalizeDir(dirPath);
|
|
1019
|
+
const subdirs = /* @__PURE__ */ new Set();
|
|
1020
|
+
for (const d of docs) {
|
|
1021
|
+
if (!d.filepath.startsWith(dir)) continue;
|
|
1022
|
+
const rest = d.filepath.slice(dir.length);
|
|
1023
|
+
const slashIdx = rest.indexOf("/");
|
|
1024
|
+
if (slashIdx === -1) continue;
|
|
1025
|
+
subdirs.add(rest.slice(0, slashIdx));
|
|
1026
|
+
}
|
|
1027
|
+
if (subdirs.size === 0) {
|
|
1028
|
+
return "## Subdirectories\n_(none)_";
|
|
1029
|
+
}
|
|
1030
|
+
const sorted = [...subdirs].sort();
|
|
1031
|
+
const blocks = sorted.map((s) => renderSubdirOutline(dir, s, docs));
|
|
1032
|
+
return `## Subdirectories
|
|
1033
|
+
${blocks.join("\n\n")}`;
|
|
1034
|
+
}
|
|
1035
|
+
function generateKeywordsSection(docs) {
|
|
1036
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1037
|
+
for (const d of docs) {
|
|
1038
|
+
const all = [...d.keywords ?? [], ...d.tags ?? []];
|
|
1039
|
+
const fileName = basename2(d.filepath);
|
|
1040
|
+
for (const term of all) {
|
|
1041
|
+
if (typeof term !== "string" || term.length === 0) continue;
|
|
1042
|
+
const key = term.toLowerCase();
|
|
1043
|
+
let entry = groups.get(key);
|
|
1044
|
+
if (!entry) {
|
|
1045
|
+
entry = { variants: [], files: /* @__PURE__ */ new Set() };
|
|
1046
|
+
groups.set(key, entry);
|
|
1047
|
+
}
|
|
1048
|
+
entry.variants.push(term);
|
|
1049
|
+
entry.files.add(fileName);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const rows = [];
|
|
1053
|
+
for (const entry of groups.values()) {
|
|
1054
|
+
const sortedVariants = [...entry.variants].sort();
|
|
1055
|
+
const display = sortedVariants[0];
|
|
1056
|
+
const files = [...entry.files].sort();
|
|
1057
|
+
rows.push({ display, files });
|
|
1058
|
+
}
|
|
1059
|
+
rows.sort((a, b) => {
|
|
1060
|
+
const la = a.display.toLowerCase();
|
|
1061
|
+
const lb = b.display.toLowerCase();
|
|
1062
|
+
if (la < lb) return -1;
|
|
1063
|
+
if (la > lb) return 1;
|
|
1064
|
+
return 0;
|
|
1065
|
+
});
|
|
1066
|
+
if (rows.length === 0) {
|
|
1067
|
+
return `## Keywords
|
|
1068
|
+
${KEYWORDS_SENTINEL}
|
|
1069
|
+
_(no keywords)_`;
|
|
1070
|
+
}
|
|
1071
|
+
const bullets = rows.map((r) => `- ${r.display} \u2014 ${r.files.join(", ")}`).join("\n");
|
|
1072
|
+
return `## Keywords
|
|
1073
|
+
${KEYWORDS_SENTINEL}
|
|
1074
|
+
${bullets}`;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/agents-md/merge.ts
|
|
1078
|
+
var IF_WORKING_HERE_TITLE = "If you're working here";
|
|
1079
|
+
var KEYWORDS_TITLE = "Keywords";
|
|
1080
|
+
var NOTES_FOR_AGENTS_TITLE = "Notes for agents";
|
|
1081
|
+
var PRESERVE_OPEN = "<!-- bisque-preserve -->";
|
|
1082
|
+
var PRESERVE_CLOSE = "<!-- /bisque-preserve -->";
|
|
1083
|
+
var PRESERVE_FENCE_REGEX = /<!-- bisque-preserve -->[\s\S]*?<!-- \/bisque-preserve -->/g;
|
|
1084
|
+
function mergeAgentsMd(existing, header, mechanicals) {
|
|
1085
|
+
if (!existing) {
|
|
1086
|
+
return `${header}
|
|
1087
|
+
|
|
1088
|
+
${mechanicals}
|
|
1089
|
+
`;
|
|
1090
|
+
}
|
|
1091
|
+
const sections = scanSections(existing);
|
|
1092
|
+
const keywordsSection = findSection(sections, KEYWORDS_TITLE);
|
|
1093
|
+
const userKeywords = keywordsSection && !isMechanicalKeywords(keywordsSection) ? trimTrailingBlank(keywordsSection.text) : null;
|
|
1094
|
+
const composedMechanicals = userKeywords ? stripMechanicalKeywordsBlock(mechanicals) : mechanicals;
|
|
1095
|
+
const notesSection = findSection(sections, NOTES_FOR_AGENTS_TITLE);
|
|
1096
|
+
const notesBlock = notesSection ? buildWrappedNotesBlock(notesSection.text) : null;
|
|
1097
|
+
const ifWorkingHereSection = findSection(sections, IF_WORKING_HERE_TITLE);
|
|
1098
|
+
const ifWorkingHereBlock = ifWorkingHereSection ? trimTrailingBlank(ifWorkingHereSection.text) : null;
|
|
1099
|
+
const allFenceRanges = findAllPreserveFenceRanges(existing);
|
|
1100
|
+
const coveredRanges = [];
|
|
1101
|
+
if (notesSection) coveredRanges.push({ start: notesSection.start, end: notesSection.end });
|
|
1102
|
+
if (keywordsSection && userKeywords) {
|
|
1103
|
+
coveredRanges.push({ start: keywordsSection.start, end: keywordsSection.end });
|
|
1104
|
+
}
|
|
1105
|
+
if (ifWorkingHereSection) {
|
|
1106
|
+
coveredRanges.push({ start: ifWorkingHereSection.start, end: ifWorkingHereSection.end });
|
|
1107
|
+
}
|
|
1108
|
+
const otherFences = allFenceRanges.filter((r) => !coveredRanges.some((c) => r.start >= c.start && r.end <= c.end)).map((r) => existing.slice(r.start, r.end));
|
|
1109
|
+
const parts = [];
|
|
1110
|
+
parts.push(header);
|
|
1111
|
+
parts.push(composedMechanicals);
|
|
1112
|
+
if (userKeywords) parts.push(userKeywords);
|
|
1113
|
+
if (notesBlock) parts.push(notesBlock);
|
|
1114
|
+
for (const fence of otherFences) parts.push(fence);
|
|
1115
|
+
if (ifWorkingHereBlock) parts.push(ifWorkingHereBlock);
|
|
1116
|
+
return `${parts.join("\n\n")}
|
|
1117
|
+
`;
|
|
1118
|
+
}
|
|
1119
|
+
function updateFenceState(line, state) {
|
|
1120
|
+
const trimmedLeading = line.replace(/^[ \t]+/, "");
|
|
1121
|
+
const isBacktick = trimmedLeading.startsWith("```");
|
|
1122
|
+
const isTilde = trimmedLeading.startsWith("~~~");
|
|
1123
|
+
if (!isBacktick && !isTilde) {
|
|
1124
|
+
return { state, isFenceLine: false };
|
|
1125
|
+
}
|
|
1126
|
+
const marker = isBacktick ? "```" : "~~~";
|
|
1127
|
+
if (!state.inFence) {
|
|
1128
|
+
return { state: { inFence: true, marker }, isFenceLine: true };
|
|
1129
|
+
}
|
|
1130
|
+
if (state.marker === marker) {
|
|
1131
|
+
return { state: { inFence: false, marker: null }, isFenceLine: true };
|
|
1132
|
+
}
|
|
1133
|
+
return { state, isFenceLine: true };
|
|
1134
|
+
}
|
|
1135
|
+
function tryParseHeading(line) {
|
|
1136
|
+
if (/^[ \t]*>/.test(line)) return null;
|
|
1137
|
+
const m = /^[ \t]*##[ \t]+(.+?)[ \t]*$/.exec(line);
|
|
1138
|
+
return m ? m[1] ?? "" : null;
|
|
1139
|
+
}
|
|
1140
|
+
function computeLineStarts(lines) {
|
|
1141
|
+
const starts = [];
|
|
1142
|
+
let offset = 0;
|
|
1143
|
+
for (const line of lines) {
|
|
1144
|
+
starts.push(offset);
|
|
1145
|
+
offset += line.length + 1;
|
|
1146
|
+
}
|
|
1147
|
+
return starts;
|
|
1148
|
+
}
|
|
1149
|
+
function scanSections(text) {
|
|
1150
|
+
const lines = text.split("\n");
|
|
1151
|
+
const lineStarts = computeLineStarts(lines);
|
|
1152
|
+
const headings = [];
|
|
1153
|
+
let fenceState = { inFence: false, marker: null };
|
|
1154
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1155
|
+
const raw = lines[i] ?? "";
|
|
1156
|
+
const fence = updateFenceState(raw, fenceState);
|
|
1157
|
+
fenceState = fence.state;
|
|
1158
|
+
if (fence.isFenceLine || fenceState.inFence) continue;
|
|
1159
|
+
const title = tryParseHeading(raw);
|
|
1160
|
+
if (title === null) continue;
|
|
1161
|
+
headings.push({ title, byteStart: lineStarts[i] ?? 0 });
|
|
1162
|
+
}
|
|
1163
|
+
const sections = [];
|
|
1164
|
+
for (let h = 0; h < headings.length; h++) {
|
|
1165
|
+
const current = headings[h];
|
|
1166
|
+
const next = headings[h + 1];
|
|
1167
|
+
const end = next ? next.byteStart : text.length;
|
|
1168
|
+
sections.push({
|
|
1169
|
+
title: current.title,
|
|
1170
|
+
start: current.byteStart,
|
|
1171
|
+
end,
|
|
1172
|
+
text: text.slice(current.byteStart, end)
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
return sections;
|
|
1176
|
+
}
|
|
1177
|
+
function findSection(sections, title) {
|
|
1178
|
+
for (const s of sections) {
|
|
1179
|
+
if (s.title === title) return s;
|
|
1180
|
+
}
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
function isMechanicalKeywords(section) {
|
|
1184
|
+
const lines = section.text.split("\n");
|
|
1185
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1186
|
+
const line = lines[i] ?? "";
|
|
1187
|
+
if (line.trim() === "") continue;
|
|
1188
|
+
return line === KEYWORDS_SENTINEL;
|
|
1189
|
+
}
|
|
1190
|
+
return true;
|
|
1191
|
+
}
|
|
1192
|
+
function stripMechanicalKeywordsBlock(mechanicals) {
|
|
1193
|
+
const sections = scanSections(mechanicals);
|
|
1194
|
+
const kw = findSection(sections, KEYWORDS_TITLE);
|
|
1195
|
+
if (!kw) return mechanicals;
|
|
1196
|
+
if (!isMechanicalKeywords(kw)) return mechanicals;
|
|
1197
|
+
let removeStart = kw.start;
|
|
1198
|
+
let removeEnd = kw.end;
|
|
1199
|
+
if (removeStart >= 2 && mechanicals.slice(removeStart - 2, removeStart) === "\n\n") {
|
|
1200
|
+
removeStart -= 1;
|
|
1201
|
+
}
|
|
1202
|
+
if (removeEnd < mechanicals.length && mechanicals[removeEnd] === "\n") {
|
|
1203
|
+
removeEnd += 1;
|
|
1204
|
+
}
|
|
1205
|
+
const out = mechanicals.slice(0, removeStart) + mechanicals.slice(removeEnd);
|
|
1206
|
+
return out.replace(/\n+$/, "");
|
|
1207
|
+
}
|
|
1208
|
+
function buildWrappedNotesBlock(sectionText) {
|
|
1209
|
+
const trimmed = trimTrailingBlank(sectionText);
|
|
1210
|
+
const lines = trimmed.split("\n");
|
|
1211
|
+
const heading = lines[0] ?? `## ${NOTES_FOR_AGENTS_TITLE}`;
|
|
1212
|
+
const body = lines.slice(1).join("\n");
|
|
1213
|
+
if (body.includes(PRESERVE_OPEN) && body.includes(PRESERVE_CLOSE)) {
|
|
1214
|
+
return trimmed;
|
|
1215
|
+
}
|
|
1216
|
+
const bodyTrimmedLeading = body.replace(/^\n+/, "");
|
|
1217
|
+
if (bodyTrimmedLeading.length === 0) {
|
|
1218
|
+
return `${heading}
|
|
1219
|
+
${PRESERVE_OPEN}
|
|
1220
|
+
${PRESERVE_CLOSE}`;
|
|
1221
|
+
}
|
|
1222
|
+
return `${heading}
|
|
1223
|
+
${PRESERVE_OPEN}
|
|
1224
|
+
${bodyTrimmedLeading}
|
|
1225
|
+
${PRESERVE_CLOSE}`;
|
|
1226
|
+
}
|
|
1227
|
+
function findAllPreserveFenceRanges(text) {
|
|
1228
|
+
const ranges = [];
|
|
1229
|
+
const re = new RegExp(PRESERVE_FENCE_REGEX.source, PRESERVE_FENCE_REGEX.flags);
|
|
1230
|
+
let m = re.exec(text);
|
|
1231
|
+
while (m) {
|
|
1232
|
+
ranges.push({ start: m.index, end: m.index + m[0].length });
|
|
1233
|
+
m = re.exec(text);
|
|
1234
|
+
}
|
|
1235
|
+
return ranges;
|
|
1236
|
+
}
|
|
1237
|
+
function trimTrailingBlank(s) {
|
|
1238
|
+
return s.replace(/\n+$/, "");
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/commands/agents.ts
|
|
1242
|
+
init_api_client();
|
|
1243
|
+
|
|
1244
|
+
// src/utils/exit-codes.ts
|
|
1245
|
+
init_src();
|
|
1246
|
+
function exitWithPickupModeDeniedError(expected, actual) {
|
|
1247
|
+
console.error(`Error: Task requires claimant class ${expected} but claim asserted ${actual}.`);
|
|
1248
|
+
process.exit(4 /* POLICY_DENIED */);
|
|
1249
|
+
}
|
|
1250
|
+
function exitWithRoleDeniedError(err) {
|
|
1251
|
+
console.error(
|
|
1252
|
+
`Error: Insufficient workspace role \u2014 '${err.expected}' required, you have '${err.actual}'.`
|
|
1253
|
+
);
|
|
1254
|
+
process.exit(4 /* POLICY_DENIED */);
|
|
1255
|
+
}
|
|
1256
|
+
function exitWithWorkspaceScopeDeniedError(err) {
|
|
1257
|
+
console.error(
|
|
1258
|
+
`Error: OAuth token is scoped to workspace '${err.expected}', but the request targets '${err.actual}'. Switch workspaces in the host app or re-issue the token.`
|
|
1259
|
+
);
|
|
1260
|
+
process.exit(4 /* POLICY_DENIED */);
|
|
1261
|
+
}
|
|
1262
|
+
function exitWithWorkspaceContextRequiredError(err) {
|
|
1263
|
+
const detail = err.message ? `: ${err.message}` : "";
|
|
1264
|
+
console.error(
|
|
1265
|
+
`Error: Workspace context required${detail}. Re-run with --workspace <id>, set BISQUE_WORKSPACE_ID, or use 'bisque workspace switch <id>'.`
|
|
1266
|
+
);
|
|
1267
|
+
process.exit(5 /* WORKSPACE_CONTEXT_REQUIRED */);
|
|
1268
|
+
}
|
|
1269
|
+
function exitWithInviteRedeemFailedError(err) {
|
|
1270
|
+
const reasonText = {
|
|
1271
|
+
"not-found": "Invite code not found \u2014 check for typos.",
|
|
1272
|
+
expired: "Invite code has expired.",
|
|
1273
|
+
revoked: "Invite code has been revoked.",
|
|
1274
|
+
exhausted: "Invite code has no remaining redemptions.",
|
|
1275
|
+
"transaction-conflict": "Transient race redeeming invite \u2014 retry shortly."
|
|
1276
|
+
};
|
|
1277
|
+
const human = reasonText[err.reason] ?? `Invite redemption failed: ${err.reason}`;
|
|
1278
|
+
console.error(`Error: ${human}`);
|
|
1279
|
+
process.exit(6 /* INVITE_REDEEM_FAILED */);
|
|
1280
|
+
}
|
|
1281
|
+
function parsePickupModeDeniedFromMessage(message) {
|
|
1282
|
+
if (!message.includes("403") || !message.includes("pickup-mode-denied")) return null;
|
|
1283
|
+
const jsonStart = message.indexOf("{");
|
|
1284
|
+
if (jsonStart === -1) return null;
|
|
1285
|
+
try {
|
|
1286
|
+
const body = JSON.parse(message.slice(jsonStart));
|
|
1287
|
+
if (body["error"] !== "pickup-mode-denied") return null;
|
|
1288
|
+
if (typeof body["expected"] !== "string" || typeof body["actual"] !== "string") return null;
|
|
1289
|
+
return {
|
|
1290
|
+
expected: body["expected"],
|
|
1291
|
+
actual: body["actual"]
|
|
1292
|
+
};
|
|
1293
|
+
} catch {
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
function parseEnvelopeFromMessage(message, expectedStatus, expectedError) {
|
|
1298
|
+
if (!message.includes(`${expectedStatus}`) || !message.includes(expectedError)) return null;
|
|
1299
|
+
const jsonStart = message.indexOf("{");
|
|
1300
|
+
if (jsonStart === -1) return null;
|
|
1301
|
+
try {
|
|
1302
|
+
const body = JSON.parse(message.slice(jsonStart));
|
|
1303
|
+
if (body["error"] !== expectedError) return null;
|
|
1304
|
+
return body;
|
|
1305
|
+
} catch {
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
function exitWithPlanLimitError(err) {
|
|
1310
|
+
if (err.code === "QUOTA_EXCEEDED") {
|
|
1311
|
+
const used = err.extras["used"] ?? "?";
|
|
1312
|
+
const limit = err.extras["limit"] ?? err.limit;
|
|
1313
|
+
const resetsAt = err.extras["resetsAt"] ?? "next month";
|
|
1314
|
+
console.error(`Bisque: monthly API quota exceeded (${used}/${limit}).`);
|
|
1315
|
+
console.error(` Reads still work; writes resume at ${resetsAt}.`);
|
|
1316
|
+
console.error(` Pro access is invite-only \u2014 request: mailto:beta@bisque.app`);
|
|
1317
|
+
} else {
|
|
1318
|
+
const gate = err.extras["gate"] ?? "plan";
|
|
1319
|
+
console.error(`Bisque: ${err.code} (${gate} limit on ${err.plan}).`);
|
|
1320
|
+
console.error(` Pro access is invite-only \u2014 request: mailto:beta@bisque.app`);
|
|
1321
|
+
}
|
|
1322
|
+
process.exit(1 /* GENERAL_ERROR */);
|
|
1323
|
+
}
|
|
1324
|
+
function tryTypedInstanceBranches(error) {
|
|
1325
|
+
if (error instanceof PickupModeDeniedError) {
|
|
1326
|
+
exitWithPickupModeDeniedError(error.expected, error.actual);
|
|
1327
|
+
}
|
|
1328
|
+
if (error instanceof RoleDeniedError) {
|
|
1329
|
+
exitWithRoleDeniedError(error);
|
|
1330
|
+
}
|
|
1331
|
+
if (error instanceof WorkspaceScopeDeniedError) {
|
|
1332
|
+
exitWithWorkspaceScopeDeniedError(error);
|
|
1333
|
+
}
|
|
1334
|
+
if (error instanceof WorkspaceContextRequiredError) {
|
|
1335
|
+
exitWithWorkspaceContextRequiredError(error);
|
|
1336
|
+
}
|
|
1337
|
+
if (error instanceof InviteRedeemFailedError) {
|
|
1338
|
+
exitWithInviteRedeemFailedError(error);
|
|
1339
|
+
}
|
|
1340
|
+
if (error instanceof PlanLimitError) {
|
|
1341
|
+
exitWithPlanLimitError(error);
|
|
1342
|
+
}
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
function tryRoleDeniedDecoder(message) {
|
|
1346
|
+
const parsed = parseEnvelopeFromMessage(message, 403, "role-denied");
|
|
1347
|
+
if (!parsed) return;
|
|
1348
|
+
if (typeof parsed["expected"] !== "string" || typeof parsed["actual"] !== "string") return;
|
|
1349
|
+
exitWithRoleDeniedError(
|
|
1350
|
+
new RoleDeniedError(
|
|
1351
|
+
parsed["expected"],
|
|
1352
|
+
parsed["actual"],
|
|
1353
|
+
typeof parsed["message"] === "string" ? parsed["message"] : void 0
|
|
1354
|
+
)
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
function tryWorkspaceScopeDeniedDecoder(message) {
|
|
1358
|
+
const parsed = parseEnvelopeFromMessage(message, 403, "workspace-scope-denied");
|
|
1359
|
+
if (!parsed) return;
|
|
1360
|
+
if (typeof parsed["expected"] !== "string" || typeof parsed["actual"] !== "string") return;
|
|
1361
|
+
exitWithWorkspaceScopeDeniedError(
|
|
1362
|
+
new WorkspaceScopeDeniedError(
|
|
1363
|
+
parsed["expected"],
|
|
1364
|
+
parsed["actual"],
|
|
1365
|
+
typeof parsed["message"] === "string" ? parsed["message"] : void 0
|
|
1366
|
+
)
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
function tryWorkspaceContextRequiredDecoder(message) {
|
|
1370
|
+
const parsed = parseEnvelopeFromMessage(message, 400, "workspace-context-required");
|
|
1371
|
+
if (!parsed) return;
|
|
1372
|
+
const ctxMessage = typeof parsed["message"] === "string" ? parsed["message"] : void 0;
|
|
1373
|
+
exitWithWorkspaceContextRequiredError(new WorkspaceContextRequiredError(ctxMessage));
|
|
1374
|
+
}
|
|
1375
|
+
function tryInviteRedeemFailedDecoder(message) {
|
|
1376
|
+
const parsed = parseEnvelopeFromMessage(message, 409, "invite-redeem-failed");
|
|
1377
|
+
if (!parsed) return;
|
|
1378
|
+
if (typeof parsed["reason"] !== "string") return;
|
|
1379
|
+
exitWithInviteRedeemFailedError(
|
|
1380
|
+
new InviteRedeemFailedError(
|
|
1381
|
+
parsed["reason"],
|
|
1382
|
+
typeof parsed["message"] === "string" ? parsed["message"] : void 0
|
|
1383
|
+
)
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
function tryEnvelopeMessageDecoders(message) {
|
|
1387
|
+
const parsedPickup = parsePickupModeDeniedFromMessage(message);
|
|
1388
|
+
if (parsedPickup) {
|
|
1389
|
+
exitWithPickupModeDeniedError(parsedPickup.expected, parsedPickup.actual);
|
|
1390
|
+
}
|
|
1391
|
+
tryRoleDeniedDecoder(message);
|
|
1392
|
+
tryWorkspaceScopeDeniedDecoder(message);
|
|
1393
|
+
tryWorkspaceContextRequiredDecoder(message);
|
|
1394
|
+
tryInviteRedeemFailedDecoder(message);
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
function exitWithError(error) {
|
|
1398
|
+
tryTypedInstanceBranches(error);
|
|
1399
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1400
|
+
tryEnvelopeMessageDecoders(message);
|
|
1401
|
+
if (message.includes("Authentication failed") || message.includes("Unauthorized")) {
|
|
1402
|
+
console.error(`Error: ${message}`);
|
|
1403
|
+
process.exit(2 /* AUTH_ERROR */);
|
|
1404
|
+
}
|
|
1405
|
+
if (message.includes("not found") || message.includes("Not found")) {
|
|
1406
|
+
console.error(`Error: ${message}`);
|
|
1407
|
+
process.exit(3 /* NOT_FOUND */);
|
|
1408
|
+
}
|
|
1409
|
+
console.error(`Error: ${message}`);
|
|
1410
|
+
process.exit(1 /* GENERAL_ERROR */);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/commands/agents.ts
|
|
1414
|
+
function normalizeDir2(p) {
|
|
1415
|
+
return p.endsWith("/") ? p : `${p}/`;
|
|
1416
|
+
}
|
|
1417
|
+
async function listAllDocsUnderPrefix(client, dir) {
|
|
1418
|
+
const all = [];
|
|
1419
|
+
let cursor;
|
|
1420
|
+
const encodedPrefix = encodeURIComponent(dir);
|
|
1421
|
+
do {
|
|
1422
|
+
const qs = cursor ? `?pathPrefix=${encodedPrefix}&limit=200&cursor=${encodeURIComponent(cursor)}` : `?pathPrefix=${encodedPrefix}&limit=200`;
|
|
1423
|
+
const page = await client.get(`/v1/documents${qs}`);
|
|
1424
|
+
all.push(...page.items ?? []);
|
|
1425
|
+
cursor = page.cursor;
|
|
1426
|
+
} while (cursor);
|
|
1427
|
+
return all;
|
|
1428
|
+
}
|
|
1429
|
+
function isVersionConflictError(err) {
|
|
1430
|
+
if (!(err instanceof Error)) return false;
|
|
1431
|
+
const msg = err.message;
|
|
1432
|
+
return msg.includes("409") && msg.includes("version-conflict");
|
|
1433
|
+
}
|
|
1434
|
+
function composeAgentsContent(dir, items) {
|
|
1435
|
+
const docs = items.map((d) => ({
|
|
1436
|
+
filepath: d.filepath,
|
|
1437
|
+
title: d.title,
|
|
1438
|
+
summary: d.summary,
|
|
1439
|
+
// Pass through content so the renderer can fall back to a
|
|
1440
|
+
// first-paragraph extract when `summary` is empty, and so the
|
|
1441
|
+
// `## Subdirectories` outline can read each child AGENTS.md's
|
|
1442
|
+
// `**Purpose:**` line.
|
|
1443
|
+
content: d.content,
|
|
1444
|
+
keywords: d.keywords,
|
|
1445
|
+
tags: d.tags
|
|
1446
|
+
}));
|
|
1447
|
+
const inDirDocs = docs.filter((d) => {
|
|
1448
|
+
if (!d.filepath.startsWith(dir)) return false;
|
|
1449
|
+
const rest = d.filepath.slice(dir.length);
|
|
1450
|
+
if (rest.includes("/")) return false;
|
|
1451
|
+
if (rest === "AGENTS.md") return false;
|
|
1452
|
+
return true;
|
|
1453
|
+
});
|
|
1454
|
+
const existingDoc = items.find((d) => d.filepath === `${dir}AGENTS.md`);
|
|
1455
|
+
const header = generateHeader(dir);
|
|
1456
|
+
const mechanicals = `${generateWhatsHereSection(dir, docs)}
|
|
1457
|
+
|
|
1458
|
+
${generateSubdirectoriesSection(dir, docs)}
|
|
1459
|
+
|
|
1460
|
+
${generateKeywordsSection(inDirDocs)}`;
|
|
1461
|
+
const nextContent = mergeAgentsMd(existingDoc?.content ?? null, header, mechanicals);
|
|
1462
|
+
return nextContent;
|
|
1463
|
+
}
|
|
1464
|
+
async function postAgentsMd(client, dir, items) {
|
|
1465
|
+
const content = composeAgentsContent(dir, items);
|
|
1466
|
+
await client.post("/v1/documents", {
|
|
1467
|
+
title: "AGENTS.md",
|
|
1468
|
+
content,
|
|
1469
|
+
filepath: `${dir}AGENTS.md`
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
async function retryAfterVersionConflict(client, dir) {
|
|
1473
|
+
const refreshedItems = await listAllDocsUnderPrefix(client, dir);
|
|
1474
|
+
const refreshedExisting = refreshedItems.find((d) => d.filepath === `${dir}AGENTS.md`);
|
|
1475
|
+
if (!refreshedExisting) {
|
|
1476
|
+
await postAgentsMd(client, dir, refreshedItems);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const freshContent = composeAgentsContent(dir, refreshedItems);
|
|
1480
|
+
await client.put(`/v1/documents/${refreshedExisting.documentId}`, {
|
|
1481
|
+
content: freshContent,
|
|
1482
|
+
expectedVersion: refreshedExisting.version
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
async function refreshOneDirectory(client, dirPath) {
|
|
1486
|
+
const dir = normalizeDir2(dirPath);
|
|
1487
|
+
try {
|
|
1488
|
+
const items = await listAllDocsUnderPrefix(client, dir);
|
|
1489
|
+
const existingDoc = items.find((d) => d.filepath === `${dir}AGENTS.md`);
|
|
1490
|
+
const nextContent = composeAgentsContent(dir, items);
|
|
1491
|
+
if (existingDoc) {
|
|
1492
|
+
let retried = false;
|
|
1493
|
+
try {
|
|
1494
|
+
await client.put(`/v1/documents/${existingDoc.documentId}`, {
|
|
1495
|
+
content: nextContent,
|
|
1496
|
+
expectedVersion: existingDoc.version
|
|
1497
|
+
});
|
|
1498
|
+
} catch (err) {
|
|
1499
|
+
if (!isVersionConflictError(err)) throw err;
|
|
1500
|
+
retried = true;
|
|
1501
|
+
await retryAfterVersionConflict(client, dir);
|
|
1502
|
+
}
|
|
1503
|
+
return {
|
|
1504
|
+
status: "refreshed",
|
|
1505
|
+
path: dir,
|
|
1506
|
+
mode: retried ? "updated-after-retry" : "updated"
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
await postAgentsMd(client, dir, items);
|
|
1510
|
+
return { status: "refreshed", path: dir, mode: "created" };
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1513
|
+
return { status: "failed", path: dir, reason };
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
async function refreshAgentsMd(dirPath, json) {
|
|
1517
|
+
const client = new ApiClient();
|
|
1518
|
+
const outcome = await refreshOneDirectory(client, dirPath);
|
|
1519
|
+
if (outcome.status === "failed") {
|
|
1520
|
+
exitWithError(new Error(outcome.reason ?? "refresh failed"));
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const mode = outcome.mode === "created" ? "created" : "updated";
|
|
1524
|
+
if (json) {
|
|
1525
|
+
console.log(JSON.stringify({ path: outcome.path, mode }));
|
|
1526
|
+
} else {
|
|
1527
|
+
console.log(`AGENTS.md ${mode} at ${outcome.path}`);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
function discoverDirectories(root, items) {
|
|
1531
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
1532
|
+
dirs.add(root);
|
|
1533
|
+
for (const doc of items) {
|
|
1534
|
+
const fp = doc.filepath;
|
|
1535
|
+
const lastSlash = fp.lastIndexOf("/");
|
|
1536
|
+
if (lastSlash < 0) continue;
|
|
1537
|
+
const parent = `${fp.slice(0, lastSlash)}/`;
|
|
1538
|
+
if (!parent.startsWith(root)) continue;
|
|
1539
|
+
dirs.add(parent);
|
|
1540
|
+
}
|
|
1541
|
+
return Array.from(dirs).sort((a, b) => a.localeCompare(b));
|
|
1542
|
+
}
|
|
1543
|
+
async function refreshAgentsTree(rootPath, json) {
|
|
1544
|
+
const summary = { refreshed: [], skipped: [], failed: [] };
|
|
1545
|
+
try {
|
|
1546
|
+
const client = new ApiClient();
|
|
1547
|
+
const root = normalizeDir2(rootPath);
|
|
1548
|
+
let allDocs;
|
|
1549
|
+
try {
|
|
1550
|
+
allDocs = await listAllDocsUnderPrefix(client, root);
|
|
1551
|
+
} catch (err) {
|
|
1552
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1553
|
+
summary.failed.push({ path: root, reason });
|
|
1554
|
+
emitTreeSummary(summary, json);
|
|
1555
|
+
process.exitCode = 1;
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
const directories = discoverDirectories(root, allDocs);
|
|
1559
|
+
for (const dir of directories) {
|
|
1560
|
+
const outcome = await refreshOneDirectory(client, dir);
|
|
1561
|
+
if (outcome.status === "refreshed") {
|
|
1562
|
+
summary.refreshed.push(outcome.path);
|
|
1563
|
+
} else if (outcome.status === "skipped") {
|
|
1564
|
+
summary.skipped.push(outcome.path);
|
|
1565
|
+
} else {
|
|
1566
|
+
summary.failed.push({
|
|
1567
|
+
path: outcome.path,
|
|
1568
|
+
reason: outcome.reason ?? "unknown"
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1574
|
+
summary.failed.push({ path: rootPath, reason });
|
|
1575
|
+
}
|
|
1576
|
+
emitTreeSummary(summary, json);
|
|
1577
|
+
if (summary.failed.length > 0) {
|
|
1578
|
+
process.exitCode = 1;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
function emitTreeSummary(summary, json) {
|
|
1582
|
+
if (json) {
|
|
1583
|
+
console.log(JSON.stringify(summary));
|
|
1584
|
+
} else {
|
|
1585
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function registerAgentsCommand(program2) {
|
|
1589
|
+
const agents = program2.command("agents").description("Maintain per-directory AGENTS.md files");
|
|
1590
|
+
agents.command("refresh <path>").description("Regenerate the mechanical sections of AGENTS.md at <path>").option("--json", "Output raw JSON result").action(async (path2, options) => {
|
|
1591
|
+
await refreshAgentsMd(path2, options.json ?? false);
|
|
1592
|
+
});
|
|
1593
|
+
agents.command("refresh-tree <root>").description("Recursively refresh AGENTS.md across every directory under <root>").option("--no-json", "Pretty-print the summary instead of emitting a single-line JSON object").action(async (root, options) => {
|
|
1594
|
+
await refreshAgentsTree(root, options.json !== false);
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/commands/auth.ts
|
|
1599
|
+
init_config();
|
|
1600
|
+
import { randomBytes } from "node:crypto";
|
|
1601
|
+
import { createServer } from "node:http";
|
|
1602
|
+
function registerAuthCommand(program2) {
|
|
1603
|
+
const authCmd = program2.command("auth").description("Manage authentication");
|
|
1604
|
+
authCmd.command("login").description("Authenticate with Bisque via browser OAuth").option("-f, --force", "Force re-authentication even if already logged in").action(async (options) => {
|
|
1605
|
+
const config = new Config();
|
|
1606
|
+
const existingToken = await config.get("authToken");
|
|
1607
|
+
if (existingToken && !options.force) {
|
|
1608
|
+
console.log("Already authenticated. Use --force to re-authenticate.");
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
console.log("Starting authentication flow...");
|
|
1612
|
+
const state = randomBytes(32).toString("base64url");
|
|
1613
|
+
const { server, portReady, codePromise } = startCallbackServer(state);
|
|
1614
|
+
const port = await portReady;
|
|
1615
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
1616
|
+
const webAppUrl = await config.get("webAppUrl");
|
|
1617
|
+
const loginUrl = `${webAppUrl}/cli-link?redirect_uri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent(state)}`;
|
|
1618
|
+
console.log("Opening browser for authentication...");
|
|
1619
|
+
console.log(`If the browser doesn't open, visit: ${loginUrl}
|
|
1620
|
+
`);
|
|
1621
|
+
const { default: open } = await import("open");
|
|
1622
|
+
await open(loginUrl);
|
|
1623
|
+
let code;
|
|
1624
|
+
try {
|
|
1625
|
+
code = await codePromise;
|
|
1626
|
+
} finally {
|
|
1627
|
+
server.close();
|
|
1628
|
+
}
|
|
1629
|
+
const apiEndpoint = await config.get("apiEndpoint");
|
|
1630
|
+
const exchangeUrl = `${apiEndpoint}/v1/auth/exchange`;
|
|
1631
|
+
const res = await fetch(exchangeUrl, {
|
|
1632
|
+
method: "POST",
|
|
1633
|
+
headers: { "Content-Type": "application/json" },
|
|
1634
|
+
body: JSON.stringify({ code })
|
|
1635
|
+
});
|
|
1636
|
+
if (!res.ok) {
|
|
1637
|
+
const body = await res.text();
|
|
1638
|
+
console.error(`Exchange failed (${res.status}): ${body}`);
|
|
1639
|
+
process.exitCode = 1;
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
const parsed = await res.json();
|
|
1643
|
+
await config.set("authToken", parsed.key);
|
|
1644
|
+
console.log("Authentication successful! API key stored.");
|
|
1645
|
+
});
|
|
1646
|
+
authCmd.command("logout").description("Clear stored authentication token").action(async () => {
|
|
1647
|
+
const config = new Config();
|
|
1648
|
+
const token = await config.get("authToken");
|
|
1649
|
+
if (!token) {
|
|
1650
|
+
console.log("Not currently authenticated.");
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
await config.set("authToken", void 0);
|
|
1654
|
+
console.log("Logged out successfully. Token cleared.");
|
|
1655
|
+
});
|
|
1656
|
+
authCmd.command("status").description("Show current authentication status").action(async () => {
|
|
1657
|
+
const config = new Config();
|
|
1658
|
+
const token = await config.get("authToken");
|
|
1659
|
+
const endpoint = await config.get("apiEndpoint");
|
|
1660
|
+
if (!token) {
|
|
1661
|
+
console.log("Status: Not authenticated");
|
|
1662
|
+
console.log("Run `bisque auth login` to authenticate.");
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const masked = `${token.slice(0, 8)}...${"*".repeat(8)}`;
|
|
1666
|
+
console.log(`Token: ${masked}`);
|
|
1667
|
+
console.log(`API: ${endpoint}`);
|
|
1668
|
+
try {
|
|
1669
|
+
const { ApiClient: ApiClient2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
|
|
1670
|
+
const client = new ApiClient2(config);
|
|
1671
|
+
const me = await client.get("/v1/auth/me");
|
|
1672
|
+
console.log(`User: ${me.userId}`);
|
|
1673
|
+
console.log("Status: Authenticated");
|
|
1674
|
+
} catch {
|
|
1675
|
+
console.log("Status: Token stored but could not verify (server may be unreachable)");
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
function startCallbackServer(expectedState) {
|
|
1680
|
+
let resolveCode;
|
|
1681
|
+
let rejectCode;
|
|
1682
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
1683
|
+
resolveCode = resolve;
|
|
1684
|
+
rejectCode = reject;
|
|
1685
|
+
});
|
|
1686
|
+
const server = createServer((req, res) => {
|
|
1687
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1688
|
+
if (url.pathname !== "/callback") {
|
|
1689
|
+
res.writeHead(404);
|
|
1690
|
+
res.end("Not found");
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const code = url.searchParams.get("code");
|
|
1694
|
+
const state = url.searchParams.get("state");
|
|
1695
|
+
if (!code || !state) {
|
|
1696
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1697
|
+
res.end(
|
|
1698
|
+
"<html><body><h1>Authentication failed</h1><p>Missing code or state in callback.</p></body></html>"
|
|
1699
|
+
);
|
|
1700
|
+
rejectCode(new Error("Missing code or state in callback"));
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (state !== expectedState) {
|
|
1704
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1705
|
+
res.end(
|
|
1706
|
+
"<html><body><h1>Authentication failed</h1><p>CSRF state mismatch.</p></body></html>"
|
|
1707
|
+
);
|
|
1708
|
+
rejectCode(new Error("CSRF state mismatch \u2014 aborting for safety"));
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1712
|
+
res.end("<html><body><h1>Signed in.</h1><p>You can close this tab.</p></body></html>");
|
|
1713
|
+
resolveCode(code);
|
|
1714
|
+
});
|
|
1715
|
+
const portReady = new Promise((resolve, reject) => {
|
|
1716
|
+
server.once("error", reject);
|
|
1717
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1718
|
+
const addr = server.address();
|
|
1719
|
+
resolve(addr.port);
|
|
1720
|
+
});
|
|
1721
|
+
});
|
|
1722
|
+
const timeout = setTimeout(() => {
|
|
1723
|
+
rejectCode(new Error("Authentication timed out after 2 minutes"));
|
|
1724
|
+
server.close();
|
|
1725
|
+
}, 12e4);
|
|
1726
|
+
server.on("close", () => {
|
|
1727
|
+
clearTimeout(timeout);
|
|
1728
|
+
});
|
|
1729
|
+
return { server, portReady, codePromise };
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// src/commands/batch.ts
|
|
1733
|
+
init_api_client();
|
|
1734
|
+
import chalk from "chalk";
|
|
1735
|
+
function printDocuments(docs) {
|
|
1736
|
+
if (!docs || docs.length === 0) return;
|
|
1737
|
+
for (const doc of docs) {
|
|
1738
|
+
console.log(chalk.blue(`--- ${doc.filepath} ---`));
|
|
1739
|
+
console.log(chalk.dim(`Title: ${doc.title}`));
|
|
1740
|
+
console.log(doc.content);
|
|
1741
|
+
console.log();
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
function printList(items, label, color) {
|
|
1745
|
+
if (!items || items.length === 0) return;
|
|
1746
|
+
console.log(color(`${label}:`));
|
|
1747
|
+
for (const item of items) {
|
|
1748
|
+
console.log(color(` ${item}`));
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
function registerBatchCommand(program2) {
|
|
1752
|
+
program2.command("batch <filepaths...>").description("Fetch multiple documents by filepath in a single call").option("--workspace <id>", "Workspace ID", "default").option("--json", "Output as JSON").action(
|
|
1753
|
+
async (filepaths, options) => {
|
|
1754
|
+
try {
|
|
1755
|
+
const client = new ApiClient();
|
|
1756
|
+
const result = await client.post("/v1/documents/batch", {
|
|
1757
|
+
filepaths,
|
|
1758
|
+
workspace: options.workspace
|
|
1759
|
+
});
|
|
1760
|
+
if (options.json) {
|
|
1761
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
printDocuments(result.documents);
|
|
1765
|
+
printList(result.missing, "Missing", chalk.yellow);
|
|
1766
|
+
printList(result.failed, "Failed", chalk.red);
|
|
1767
|
+
} catch (err) {
|
|
1768
|
+
exitWithError(err);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/commands/bulk-add.ts
|
|
1775
|
+
init_api_client();
|
|
1776
|
+
import { readdir, readFile as readFile3, stat } from "node:fs/promises";
|
|
1777
|
+
import { basename as basename3, join as join2, relative } from "node:path";
|
|
1778
|
+
function slugify2(name) {
|
|
1779
|
+
return name.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-_]/g, "");
|
|
1780
|
+
}
|
|
1781
|
+
async function findFiles(dir) {
|
|
1782
|
+
const results = [];
|
|
1783
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1784
|
+
for (const entry of entries) {
|
|
1785
|
+
const fullPath = join2(dir, entry.name);
|
|
1786
|
+
if (entry.isDirectory()) {
|
|
1787
|
+
const nested = await findFiles(fullPath);
|
|
1788
|
+
results.push(...nested);
|
|
1789
|
+
} else if (entry.isFile() && /\.(md|txt)$/i.test(entry.name)) {
|
|
1790
|
+
results.push(fullPath);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
return results;
|
|
1794
|
+
}
|
|
1795
|
+
function buildDocument(file, directory, content, options) {
|
|
1796
|
+
const filename = basename3(file);
|
|
1797
|
+
const relativePath = relative(directory, file);
|
|
1798
|
+
const prefix = options.project ? `1-Projects/${slugify2(options.project)}` : "4-Archive";
|
|
1799
|
+
const filepath = `${prefix}/${relativePath}`;
|
|
1800
|
+
const category = options.project ? "project" : "archive";
|
|
1801
|
+
return { title: filename, filepath, category, tags: options.tags ?? [], content };
|
|
1802
|
+
}
|
|
1803
|
+
async function buildBatch(files, directory, options) {
|
|
1804
|
+
const documents = [];
|
|
1805
|
+
for (const file of files) {
|
|
1806
|
+
const content = await readFile3(file, "utf-8");
|
|
1807
|
+
documents.push(buildDocument(file, directory, content, options));
|
|
1808
|
+
}
|
|
1809
|
+
return documents;
|
|
1810
|
+
}
|
|
1811
|
+
var BATCH_SIZE = 25;
|
|
1812
|
+
async function executeBulkAdd(directory, options) {
|
|
1813
|
+
const dirStat = await stat(directory);
|
|
1814
|
+
if (!dirStat.isDirectory()) {
|
|
1815
|
+
console.error(`Error: ${directory} is not a directory`);
|
|
1816
|
+
process.exitCode = 1;
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
const files = await findFiles(directory);
|
|
1820
|
+
if (files.length === 0) {
|
|
1821
|
+
console.log("No .md or .txt files found.");
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
const client = new ApiClient();
|
|
1825
|
+
let uploaded = 0;
|
|
1826
|
+
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
|
1827
|
+
const batch = files.slice(i, i + BATCH_SIZE);
|
|
1828
|
+
const documents = await buildBatch(batch, directory, options);
|
|
1829
|
+
await client.post("/v1/documents/bulk", { documents });
|
|
1830
|
+
uploaded += documents.length;
|
|
1831
|
+
console.log(`Progress: ${uploaded}/${files.length}`);
|
|
1832
|
+
}
|
|
1833
|
+
console.log(`Done. Added ${uploaded} documents.`);
|
|
1834
|
+
}
|
|
1835
|
+
function registerBulkAddCommand(program2) {
|
|
1836
|
+
program2.command("bulk-add <directory>").description("Recursively add .md and .txt files from a directory").option("-p, --project <name>", "Place all documents under a PARA project").option(
|
|
1837
|
+
"-t, --tags <tags>",
|
|
1838
|
+
"Comma-separated tags",
|
|
1839
|
+
(val) => val.split(",").map((t) => t.trim())
|
|
1840
|
+
).action(async (directory, options) => {
|
|
1841
|
+
try {
|
|
1842
|
+
await executeBulkAdd(directory, options);
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1845
|
+
console.error(`Error: ${message}`);
|
|
1846
|
+
process.exitCode = 1;
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// src/commands/chain.ts
|
|
1852
|
+
init_api_client();
|
|
1853
|
+
import chalk2 from "chalk";
|
|
1854
|
+
import Table from "cli-table3";
|
|
1855
|
+
|
|
1856
|
+
// src/utils/metadata-kv.ts
|
|
1857
|
+
init_src();
|
|
1858
|
+
|
|
1859
|
+
// src/commands/chain.ts
|
|
1860
|
+
function formatMetadataInline(metadata) {
|
|
1861
|
+
const inline = JSON.stringify(metadata);
|
|
1862
|
+
if (inline.length <= 80) {
|
|
1863
|
+
return inline;
|
|
1864
|
+
}
|
|
1865
|
+
return `
|
|
1866
|
+
${JSON.stringify(metadata, null, 2).split("\n").map((line) => ` ${line}`).join("\n")}`;
|
|
1867
|
+
}
|
|
1868
|
+
function buildOtherFieldBody(options) {
|
|
1869
|
+
const body = {};
|
|
1870
|
+
if (options.status) body["status"] = options.status;
|
|
1871
|
+
if (options.name) body["name"] = options.name;
|
|
1872
|
+
if (options.content) body["content"] = options.content;
|
|
1873
|
+
if (options.projectPath) body["projectPath"] = options.projectPath;
|
|
1874
|
+
if (options.referenceDocs) {
|
|
1875
|
+
body["referenceDocs"] = options.referenceDocs.split(",").map((s) => s.trim());
|
|
1876
|
+
}
|
|
1877
|
+
return body;
|
|
1878
|
+
}
|
|
1879
|
+
function printChainDetails(chain) {
|
|
1880
|
+
console.log(chalk2.bold("\nChain Details\n"));
|
|
1881
|
+
console.log(`${chalk2.cyan("Name:")} ${chain.name}`);
|
|
1882
|
+
console.log(`${chalk2.cyan("Status:")} ${chain.status}`);
|
|
1883
|
+
console.log(`${chalk2.cyan("ID:")} ${chain.documentId}`);
|
|
1884
|
+
if (chain.projectPath) {
|
|
1885
|
+
console.log(`${chalk2.cyan("Project Path:")} ${chain.projectPath}`);
|
|
1886
|
+
}
|
|
1887
|
+
if (chain.content) {
|
|
1888
|
+
console.log(`${chalk2.cyan("Content:")} ${chain.content}`);
|
|
1889
|
+
}
|
|
1890
|
+
if (chain.referenceDocs && chain.referenceDocs.length > 0) {
|
|
1891
|
+
console.log(`${chalk2.cyan("Reference Docs:")} ${chain.referenceDocs.join(", ")}`);
|
|
1892
|
+
}
|
|
1893
|
+
if (chain.dependencyChainIds && chain.dependencyChainIds.length > 0) {
|
|
1894
|
+
console.log(`${chalk2.cyan("Dependencies:")} ${chain.dependencyChainIds.join(", ")}`);
|
|
1895
|
+
}
|
|
1896
|
+
if (chain.metadata && Object.keys(chain.metadata).length > 0) {
|
|
1897
|
+
console.log(`${chalk2.cyan("Metadata:")} ${formatMetadataInline(chain.metadata)}`);
|
|
1898
|
+
}
|
|
1899
|
+
if (chain.createdAt) {
|
|
1900
|
+
console.log(`${chalk2.cyan("Created:")} ${new Date(chain.createdAt).toLocaleString()}`);
|
|
1901
|
+
}
|
|
1902
|
+
if (chain.updatedAt) {
|
|
1903
|
+
console.log(`${chalk2.cyan("Updated:")} ${new Date(chain.updatedAt).toLocaleString()}`);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
function printChainTasks(tasks) {
|
|
1907
|
+
if (!tasks || tasks.length === 0) return;
|
|
1908
|
+
console.log(chalk2.bold("\nTasks\n"));
|
|
1909
|
+
const taskTable = new Table({
|
|
1910
|
+
head: [chalk2.cyan("Order"), chalk2.cyan("Title"), chalk2.cyan("Status"), chalk2.cyan("ID")],
|
|
1911
|
+
style: {
|
|
1912
|
+
head: [],
|
|
1913
|
+
border: ["gray"]
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
for (const task of tasks) {
|
|
1917
|
+
taskTable.push([
|
|
1918
|
+
task.chainOrder != null ? String(task.chainOrder) : "-",
|
|
1919
|
+
task.title,
|
|
1920
|
+
task.status,
|
|
1921
|
+
task.documentId
|
|
1922
|
+
]);
|
|
1923
|
+
}
|
|
1924
|
+
console.log(taskTable.toString());
|
|
1925
|
+
}
|
|
1926
|
+
var VALID_CHAIN_STATUSES = ["active", "completed", "cancelled"];
|
|
1927
|
+
function validateChainUpdateInputs(options) {
|
|
1928
|
+
if (options.status && !VALID_CHAIN_STATUSES.includes(options.status)) {
|
|
1929
|
+
return `Error: --status must be one of: ${VALID_CHAIN_STATUSES.join(", ")} (got "${options.status}").`;
|
|
1930
|
+
}
|
|
1931
|
+
const hasSet = Object.keys(options.metadataSet ?? {}).length > 0;
|
|
1932
|
+
const hasUnset = (options.metadataUnset ?? []).length > 0;
|
|
1933
|
+
const hasMetadataMutation = hasSet || hasUnset;
|
|
1934
|
+
const hasOtherFields = Object.keys(buildOtherFieldBody(options)).length > 0;
|
|
1935
|
+
if (!hasOtherFields && !hasMetadataMutation) {
|
|
1936
|
+
return "Error: Provide at least one field to update (--status, --name, --content, --project-path, --reference-docs, --metadata-set, --metadata-unset).";
|
|
1937
|
+
}
|
|
1938
|
+
if (hasMetadataMutation && hasOtherFields) {
|
|
1939
|
+
return "Error: --metadata-set / --metadata-unset cannot be combined with other field updates. Issue two separate updates.";
|
|
1940
|
+
}
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
async function putChainMetadataMutation(client, chainId, options) {
|
|
1944
|
+
const body = {};
|
|
1945
|
+
const setEntries = options.metadataSet ?? {};
|
|
1946
|
+
const unsetKeys = options.metadataUnset ?? [];
|
|
1947
|
+
if (Object.keys(setEntries).length > 0) body["metadataSet"] = setEntries;
|
|
1948
|
+
if (unsetKeys.length > 0) body["metadataUnset"] = unsetKeys;
|
|
1949
|
+
const result = await client.put(`/v1/task-chains/${chainId}`, body);
|
|
1950
|
+
console.log(`Chain ${result.documentId} updated.`);
|
|
1951
|
+
if (result.metadata) {
|
|
1952
|
+
console.log(`Metadata: ${formatMetadataInline(result.metadata)}`);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
async function putChainOtherFields(client, chainId, options) {
|
|
1956
|
+
const result = await client.put(
|
|
1957
|
+
`/v1/task-chains/${chainId}`,
|
|
1958
|
+
buildOtherFieldBody(options)
|
|
1959
|
+
);
|
|
1960
|
+
console.log(`Chain ${result.documentId} updated.`);
|
|
1961
|
+
if (result.status) {
|
|
1962
|
+
console.log(`Status: ${result.status}`);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
function isMetadataMutationRequest(options) {
|
|
1966
|
+
const hasSet = Object.keys(options.metadataSet ?? {}).length > 0;
|
|
1967
|
+
const hasUnset = (options.metadataUnset ?? []).length > 0;
|
|
1968
|
+
return hasSet || hasUnset;
|
|
1969
|
+
}
|
|
1970
|
+
function registerChainCommand(program2) {
|
|
1971
|
+
const chain = program2.command("chain").description("Manage task chains");
|
|
1972
|
+
chain.command("create").description("Create a task chain").requiredOption("--name <name>", "Chain name").requiredOption("--content <content>", "Workstream description").option("--project-path <path>", "PARA project path").option("--reference-docs <docIds>", "Comma-separated doc UUIDs").option("--dependency-chain-ids <ids>", "Comma-separated prerequisite chain UUIDs").option(
|
|
1973
|
+
"--metadata <k=v>",
|
|
1974
|
+
"Repeatable. Free-form metadata pair. First '=' splits key from value; quote the value to preserve embedded '='.",
|
|
1975
|
+
collectMetadataPair,
|
|
1976
|
+
{}
|
|
1977
|
+
).addHelpText(
|
|
1978
|
+
"after",
|
|
1979
|
+
`
|
|
1980
|
+
Examples:
|
|
1981
|
+
$ bisque chain create --name "Build API" --content "Description"
|
|
1982
|
+
$ bisque chain create --name "Swarm chain" --content "..." \\
|
|
1983
|
+
--metadata contentShape=mechanical \\
|
|
1984
|
+
--metadata verifyCmd=./scripts/ci-check.sh
|
|
1985
|
+
`
|
|
1986
|
+
).action(
|
|
1987
|
+
async (options) => {
|
|
1988
|
+
try {
|
|
1989
|
+
const client = new ApiClient();
|
|
1990
|
+
const body = {
|
|
1991
|
+
name: options.name,
|
|
1992
|
+
content: options.content
|
|
1993
|
+
};
|
|
1994
|
+
if (options.projectPath) {
|
|
1995
|
+
body["projectPath"] = options.projectPath;
|
|
1996
|
+
}
|
|
1997
|
+
if (options.referenceDocs) {
|
|
1998
|
+
body["referenceDocs"] = options.referenceDocs.split(",").map((s) => s.trim());
|
|
1999
|
+
}
|
|
2000
|
+
if (options.dependencyChainIds) {
|
|
2001
|
+
body["dependencyChainIds"] = options.dependencyChainIds.split(",").map((s) => s.trim());
|
|
2002
|
+
}
|
|
2003
|
+
if (options.metadata && Object.keys(options.metadata).length > 0) {
|
|
2004
|
+
body["metadata"] = options.metadata;
|
|
2005
|
+
}
|
|
2006
|
+
const result = await client.post("/v1/task-chains", body);
|
|
2007
|
+
console.log(`Chain created: ${result.documentId}`);
|
|
2008
|
+
console.log(`Name: ${result.name}`);
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
exitWithError(error);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
);
|
|
2014
|
+
chain.command("list").description("List task chains").option("--status <status>", "Filter: active, completed, cancelled").option("--project-path <path>", "Filter by project path").option("--limit <n>", "Max results", "50").action(async (options) => {
|
|
2015
|
+
try {
|
|
2016
|
+
const client = new ApiClient();
|
|
2017
|
+
const params = new URLSearchParams();
|
|
2018
|
+
if (options.status) {
|
|
2019
|
+
params.set("status", options.status);
|
|
2020
|
+
}
|
|
2021
|
+
if (options.projectPath) {
|
|
2022
|
+
params.set("projectPath", options.projectPath);
|
|
2023
|
+
}
|
|
2024
|
+
params.set("limit", options.limit);
|
|
2025
|
+
const queryString = params.toString();
|
|
2026
|
+
const path2 = queryString ? `/v1/task-chains?${queryString}` : "/v1/task-chains";
|
|
2027
|
+
const chains = await client.get(path2);
|
|
2028
|
+
if (chains.length === 0) {
|
|
2029
|
+
console.log(chalk2.yellow("No chains found"));
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
const table = new Table({
|
|
2033
|
+
head: [
|
|
2034
|
+
chalk2.cyan("Name"),
|
|
2035
|
+
chalk2.cyan("Status"),
|
|
2036
|
+
chalk2.cyan("ID"),
|
|
2037
|
+
chalk2.cyan("Project Path")
|
|
2038
|
+
],
|
|
2039
|
+
style: {
|
|
2040
|
+
head: [],
|
|
2041
|
+
border: ["gray"]
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
for (const c of chains) {
|
|
2045
|
+
table.push([c.name, c.status, c.documentId, c.projectPath || "-"]);
|
|
2046
|
+
}
|
|
2047
|
+
console.log(table.toString());
|
|
2048
|
+
console.log(chalk2.gray(`
|
|
2049
|
+
Total: ${chains.length} chain(s)`));
|
|
2050
|
+
} catch (error) {
|
|
2051
|
+
exitWithError(error);
|
|
2052
|
+
}
|
|
2053
|
+
});
|
|
2054
|
+
chain.command("update <chainId>").description("Update a task chain").option("--status <status>", "New status (active|completed|cancelled)").option("--name <name>", "New name").option("--content <content>", "New content / workstream description").option("--project-path <path>", "PARA project path").option("--reference-docs <docIds>", "Comma-separated doc UUIDs").option(
|
|
2055
|
+
"--metadata-set <k=v>",
|
|
2056
|
+
"Repeatable. Additive metadata set (sibling keys preserved). First '=' splits key from value; quote the value to preserve embedded '='.",
|
|
2057
|
+
collectMetadataPair,
|
|
2058
|
+
{}
|
|
2059
|
+
).option(
|
|
2060
|
+
"--metadata-unset <key>",
|
|
2061
|
+
"Repeatable. Remove a single metadata key (sibling keys preserved).",
|
|
2062
|
+
collectUnsetKey,
|
|
2063
|
+
[]
|
|
2064
|
+
).addHelpText(
|
|
2065
|
+
"after",
|
|
2066
|
+
`
|
|
2067
|
+
Examples:
|
|
2068
|
+
$ bisque chain update abc12345-... --status completed
|
|
2069
|
+
$ bisque chain update abc12345-... --name "New chain name"
|
|
2070
|
+
$ bisque chain update abc12345-... --reference-docs doc-1,doc-2
|
|
2071
|
+
$ bisque chain update abc12345-... \\
|
|
2072
|
+
--metadata-set verifyCmd="./scripts/ci-check.sh --flag=x" \\
|
|
2073
|
+
--metadata-unset oldKey
|
|
2074
|
+
|
|
2075
|
+
Notes:
|
|
2076
|
+
--metadata-set / --metadata-unset are additive (sibling keys preserved).
|
|
2077
|
+
They cannot be combined with other field updates in the same call \u2014
|
|
2078
|
+
issue a separate update for non-metadata fields.
|
|
2079
|
+
`
|
|
2080
|
+
).action(async (chainId, options) => {
|
|
2081
|
+
try {
|
|
2082
|
+
const validationError = validateChainUpdateInputs(options);
|
|
2083
|
+
if (validationError) {
|
|
2084
|
+
console.error(validationError);
|
|
2085
|
+
process.exitCode = 1;
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
const client = new ApiClient();
|
|
2089
|
+
if (isMetadataMutationRequest(options)) {
|
|
2090
|
+
await putChainMetadataMutation(client, chainId, options);
|
|
2091
|
+
} else {
|
|
2092
|
+
await putChainOtherFields(client, chainId, options);
|
|
2093
|
+
}
|
|
2094
|
+
} catch (error) {
|
|
2095
|
+
exitWithError(error);
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
chain.command("get <chainId>").description("Get task chain details").action(async (chainId) => {
|
|
2099
|
+
try {
|
|
2100
|
+
const client = new ApiClient();
|
|
2101
|
+
const { chain: chain2, tasks } = await client.get(
|
|
2102
|
+
`/v1/task-chains/${chainId}`
|
|
2103
|
+
);
|
|
2104
|
+
printChainDetails(chain2);
|
|
2105
|
+
printChainTasks(tasks);
|
|
2106
|
+
} catch (error) {
|
|
2107
|
+
exitWithError(error);
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/commands/config.ts
|
|
2113
|
+
init_config();
|
|
2114
|
+
var VALID_KEYS = [
|
|
2115
|
+
"apiEndpoint",
|
|
2116
|
+
"webAppUrl",
|
|
2117
|
+
"authToken",
|
|
2118
|
+
"environment"
|
|
2119
|
+
];
|
|
2120
|
+
var SENSITIVE_KEYS = /* @__PURE__ */ new Set(["authToken"]);
|
|
2121
|
+
function isValidKey(key) {
|
|
2122
|
+
return VALID_KEYS.includes(key);
|
|
2123
|
+
}
|
|
2124
|
+
function maskSensitive(value) {
|
|
2125
|
+
if (value.length < 8) return "*".repeat(8);
|
|
2126
|
+
return `${value.slice(0, 8)}...${"*".repeat(8)}`;
|
|
2127
|
+
}
|
|
2128
|
+
function validateHttpUrl(key, value) {
|
|
2129
|
+
try {
|
|
2130
|
+
const url = new URL(value);
|
|
2131
|
+
if (!url.protocol.startsWith("http")) {
|
|
2132
|
+
return `Invalid ${key}. Must use HTTP or HTTPS protocol.`;
|
|
2133
|
+
}
|
|
2134
|
+
return null;
|
|
2135
|
+
} catch {
|
|
2136
|
+
return `Invalid ${key} format.`;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
function registerConfigCommand(program2) {
|
|
2140
|
+
const configCmd = program2.command("config").description("Manage CLI configuration");
|
|
2141
|
+
configCmd.command("get <key>").description(
|
|
2142
|
+
"Print a config value (sensitive values are masked; pass --reveal to print plaintext)"
|
|
2143
|
+
).option("--reveal", "Print the raw value of sensitive keys (use with care)").action(async (key, options) => {
|
|
2144
|
+
if (!isValidKey(key)) {
|
|
2145
|
+
console.error(`Invalid config key: ${key}. Valid keys: ${VALID_KEYS.join(", ")}`);
|
|
2146
|
+
process.exitCode = 1;
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
const config = new Config();
|
|
2150
|
+
const value = await config.get(key);
|
|
2151
|
+
if (value === void 0) {
|
|
2152
|
+
console.log("(not set)");
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
if (SENSITIVE_KEYS.has(key) && !options.reveal && typeof value === "string") {
|
|
2156
|
+
console.log(maskSensitive(value));
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
console.log(value);
|
|
2160
|
+
});
|
|
2161
|
+
configCmd.command("set <key> <value>").description("Set a config value").action(async (key, value) => {
|
|
2162
|
+
if (!isValidKey(key)) {
|
|
2163
|
+
console.error(`Invalid config key: ${key}. Valid keys: ${VALID_KEYS.join(", ")}`);
|
|
2164
|
+
process.exitCode = 1;
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
if (key === "apiEndpoint" || key === "webAppUrl") {
|
|
2168
|
+
const urlError = validateHttpUrl(key, value);
|
|
2169
|
+
if (urlError) {
|
|
2170
|
+
console.error(urlError);
|
|
2171
|
+
process.exitCode = 1;
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
if (key === "environment") {
|
|
2176
|
+
const validEnvs = ["dev", "staging", "prod"];
|
|
2177
|
+
if (!validEnvs.includes(value)) {
|
|
2178
|
+
console.error(`Invalid environment: ${value}. Valid values: ${validEnvs.join(", ")}`);
|
|
2179
|
+
process.exitCode = 1;
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
const config = new Config();
|
|
2184
|
+
await config.set(key, value);
|
|
2185
|
+
console.log(`Set ${key} = ${value}`);
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// src/commands/delete.ts
|
|
2190
|
+
init_api_client();
|
|
2191
|
+
import { createInterface } from "node:readline";
|
|
2192
|
+
function confirm(prompt) {
|
|
2193
|
+
const rl = createInterface({
|
|
2194
|
+
input: process.stdin,
|
|
2195
|
+
output: process.stdout
|
|
2196
|
+
});
|
|
2197
|
+
return new Promise((resolve) => {
|
|
2198
|
+
rl.question(prompt, (answer) => {
|
|
2199
|
+
rl.close();
|
|
2200
|
+
resolve(answer.toLowerCase() === "y");
|
|
2201
|
+
});
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
function registerDeleteCommand(program2) {
|
|
2205
|
+
program2.command("delete <docId>").description("Delete a document by ID").option("-f, --force", "Skip confirmation prompt").action(async (docId, options) => {
|
|
2206
|
+
try {
|
|
2207
|
+
if (!options.force) {
|
|
2208
|
+
const confirmed = await confirm(`Delete document ${docId}? (y/N) `);
|
|
2209
|
+
if (!confirmed) {
|
|
2210
|
+
console.log("Cancelled.");
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
const client = new ApiClient();
|
|
2215
|
+
await client.delete(`/v1/documents/${docId}`);
|
|
2216
|
+
console.log(`Document ${docId} deleted.`);
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2219
|
+
console.error(`Error: ${message}`);
|
|
2220
|
+
process.exitCode = 1;
|
|
2221
|
+
}
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// src/commands/get.ts
|
|
2226
|
+
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
2227
|
+
import { Option } from "commander";
|
|
2228
|
+
|
|
2229
|
+
// src/measure.ts
|
|
2230
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2231
|
+
import { homedir as homedir2 } from "node:os";
|
|
2232
|
+
import { dirname, join as join3 } from "node:path";
|
|
2233
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
2234
|
+
"status",
|
|
2235
|
+
"priority",
|
|
2236
|
+
"chainId",
|
|
2237
|
+
"chainOrder",
|
|
2238
|
+
"assignedAgent",
|
|
2239
|
+
"assignedAgentIds",
|
|
2240
|
+
"projectPath",
|
|
2241
|
+
"pathPrefix",
|
|
2242
|
+
"filepath",
|
|
2243
|
+
"path",
|
|
2244
|
+
"limit",
|
|
2245
|
+
"offset",
|
|
2246
|
+
"cursor",
|
|
2247
|
+
"since",
|
|
2248
|
+
"by",
|
|
2249
|
+
"tags",
|
|
2250
|
+
"keywords",
|
|
2251
|
+
"category",
|
|
2252
|
+
"workspaceId",
|
|
2253
|
+
"id",
|
|
2254
|
+
"documentId",
|
|
2255
|
+
"taskId",
|
|
2256
|
+
"personaId",
|
|
2257
|
+
"title",
|
|
2258
|
+
"format",
|
|
2259
|
+
"json",
|
|
2260
|
+
"dryRun",
|
|
2261
|
+
"force",
|
|
2262
|
+
"recursive",
|
|
2263
|
+
"verbose",
|
|
2264
|
+
"query",
|
|
2265
|
+
"context",
|
|
2266
|
+
"slashCommand",
|
|
2267
|
+
"preambleChars",
|
|
2268
|
+
"references"
|
|
2269
|
+
]);
|
|
2270
|
+
var STRING_CAP = 200;
|
|
2271
|
+
function measurementsPath() {
|
|
2272
|
+
return join3(homedir2(), ".bisque", "measurements.jsonl");
|
|
2273
|
+
}
|
|
2274
|
+
function capString(s) {
|
|
2275
|
+
return s.length > STRING_CAP ? s.slice(0, STRING_CAP) : s;
|
|
2276
|
+
}
|
|
2277
|
+
function isAllowedKey(key) {
|
|
2278
|
+
if (ALLOWED_KEYS.has(key)) return true;
|
|
2279
|
+
return key.endsWith("Id") || key.endsWith("Ids");
|
|
2280
|
+
}
|
|
2281
|
+
function redactReference(v) {
|
|
2282
|
+
if (!v || typeof v !== "object") return { path: "", chars: 0 };
|
|
2283
|
+
const ref = v;
|
|
2284
|
+
const path2 = typeof ref["path"] === "string" ? capString(ref["path"]) : "";
|
|
2285
|
+
const chars = typeof ref["chars"] === "number" ? ref["chars"] : 0;
|
|
2286
|
+
return { path: path2, chars };
|
|
2287
|
+
}
|
|
2288
|
+
function redactArrayValue(key, value) {
|
|
2289
|
+
if (key === "references") return value.map(redactReference);
|
|
2290
|
+
return value.map((v) => typeof v === "string" ? capString(v) : v);
|
|
2291
|
+
}
|
|
2292
|
+
function redactValue(key, value) {
|
|
2293
|
+
if (typeof value === "string") return capString(value);
|
|
2294
|
+
if (typeof value === "boolean" || typeof value === "number") return value;
|
|
2295
|
+
if (Array.isArray(value)) return redactArrayValue(key, value);
|
|
2296
|
+
return "[redacted]";
|
|
2297
|
+
}
|
|
2298
|
+
function redactArgs(args) {
|
|
2299
|
+
if (!args || typeof args !== "object") return {};
|
|
2300
|
+
const out = {};
|
|
2301
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2302
|
+
if (value === void 0) continue;
|
|
2303
|
+
if (!isAllowedKey(key)) {
|
|
2304
|
+
out[key] = "[redacted]";
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
out[key] = redactValue(key, value);
|
|
2308
|
+
}
|
|
2309
|
+
return out;
|
|
2310
|
+
}
|
|
2311
|
+
function recordMeasurement(command, args, charsReturned) {
|
|
2312
|
+
if (process.env["BISQUE_MEASURE"] !== "1") return;
|
|
2313
|
+
try {
|
|
2314
|
+
const path2 = measurementsPath();
|
|
2315
|
+
mkdirSync(dirname(path2), { recursive: true });
|
|
2316
|
+
const tokensEstimate = Math.ceil(charsReturned / 4);
|
|
2317
|
+
const line = JSON.stringify({
|
|
2318
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2319
|
+
command,
|
|
2320
|
+
args: redactArgs(args),
|
|
2321
|
+
charsReturned,
|
|
2322
|
+
tokensEstimate
|
|
2323
|
+
});
|
|
2324
|
+
appendFileSync(path2, `${line}
|
|
2325
|
+
`);
|
|
2326
|
+
} catch {
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// src/commands/get.ts
|
|
2331
|
+
init_api_client();
|
|
2332
|
+
function registerGetCommand(program2) {
|
|
2333
|
+
program2.command("get <docId>").description("Retrieve a document by ID").option("-o, --output <file>", "Save content to a file instead of stdout").addOption(
|
|
2334
|
+
new Option("--level <projection>", "Projection: outline | summary | full (default: full)").choices(["outline", "summary", "full"]).conflicts("section")
|
|
2335
|
+
).addOption(
|
|
2336
|
+
new Option(
|
|
2337
|
+
"--section <heading>",
|
|
2338
|
+
"Slice one section by heading slug (mutually exclusive with --level)"
|
|
2339
|
+
).conflicts("level")
|
|
2340
|
+
).option(
|
|
2341
|
+
"--expected-version <n>",
|
|
2342
|
+
"Optional debounce hint for --section; returns 409 if the doc has been modified"
|
|
2343
|
+
).action(
|
|
2344
|
+
async (docId, options) => {
|
|
2345
|
+
try {
|
|
2346
|
+
const client = new ApiClient();
|
|
2347
|
+
const path2 = buildPath(docId, options);
|
|
2348
|
+
const doc = await client.get(path2);
|
|
2349
|
+
recordMeasurement(
|
|
2350
|
+
"get",
|
|
2351
|
+
{ documentId: docId, ...options.level ? { level: options.level } : {} },
|
|
2352
|
+
JSON.stringify(doc).length
|
|
2353
|
+
);
|
|
2354
|
+
if (options.output) {
|
|
2355
|
+
const text = pickWritableText(doc);
|
|
2356
|
+
await writeFile2(options.output, text, "utf-8");
|
|
2357
|
+
console.log(`Document written to ${options.output}`);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
renderDoc(doc);
|
|
2361
|
+
} catch (error) {
|
|
2362
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2363
|
+
console.error(`Error: ${message}`);
|
|
2364
|
+
process.exitCode = 1;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
function buildPath(docId, options) {
|
|
2370
|
+
if (options.section !== void 0) {
|
|
2371
|
+
const params = new URLSearchParams({ heading: options.section });
|
|
2372
|
+
if (options.expectedVersion !== void 0) {
|
|
2373
|
+
params.set("expectedVersion", options.expectedVersion);
|
|
2374
|
+
}
|
|
2375
|
+
return `/v1/documents/${docId}/section?${params.toString()}`;
|
|
2376
|
+
}
|
|
2377
|
+
if (options.level) {
|
|
2378
|
+
return `/v1/documents/${docId}?level=${options.level}`;
|
|
2379
|
+
}
|
|
2380
|
+
return `/v1/documents/${docId}`;
|
|
2381
|
+
}
|
|
2382
|
+
function pickWritableText(doc) {
|
|
2383
|
+
if ("view" in doc) {
|
|
2384
|
+
if (doc.view === "summary") return doc.summary ?? "";
|
|
2385
|
+
if (doc.view === "outline") return doc.summary ?? "";
|
|
2386
|
+
if (doc.view === "section") return doc.content ?? "";
|
|
2387
|
+
}
|
|
2388
|
+
return doc.content ?? "";
|
|
2389
|
+
}
|
|
2390
|
+
function renderDoc(doc) {
|
|
2391
|
+
if ("view" in doc && doc.view === "summary") {
|
|
2392
|
+
renderSummary(doc);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
if ("view" in doc && doc.view === "outline") {
|
|
2396
|
+
renderOutline(doc);
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if ("view" in doc && doc.view === "section") {
|
|
2400
|
+
renderSection(doc);
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
renderFull(doc);
|
|
2404
|
+
}
|
|
2405
|
+
function renderSummary(doc) {
|
|
2406
|
+
console.log(`Title: ${doc.title}`);
|
|
2407
|
+
console.log(`Path: ${doc.filepath}`);
|
|
2408
|
+
console.log(`Category: ${doc.category}`);
|
|
2409
|
+
console.log(`Tags: ${doc.tags.length > 0 ? doc.tags.join(", ") : "(none)"}`);
|
|
2410
|
+
console.log(`Version: ${doc.version}`);
|
|
2411
|
+
if (doc.createdAt) console.log(`Created: ${doc.createdAt}`);
|
|
2412
|
+
if (doc.updatedAt) console.log(`Updated: ${doc.updatedAt}`);
|
|
2413
|
+
if (doc.summary) {
|
|
2414
|
+
console.log("---");
|
|
2415
|
+
console.log(doc.summary);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
function renderOutline(doc) {
|
|
2419
|
+
console.log(`Title: ${doc.title}`);
|
|
2420
|
+
console.log(`Path: ${doc.filepath}`);
|
|
2421
|
+
console.log(`Category: ${doc.category}`);
|
|
2422
|
+
console.log(`Tags: ${doc.tags.length > 0 ? doc.tags.join(", ") : "(none)"}`);
|
|
2423
|
+
console.log(`Version: ${doc.version}`);
|
|
2424
|
+
if (doc.createdAt) console.log(`Created: ${doc.createdAt}`);
|
|
2425
|
+
if (doc.updatedAt) console.log(`Updated: ${doc.updatedAt}`);
|
|
2426
|
+
console.log("---");
|
|
2427
|
+
if (doc.headings.length === 0) {
|
|
2428
|
+
console.log("(no headings)");
|
|
2429
|
+
} else {
|
|
2430
|
+
console.log("Headings:");
|
|
2431
|
+
for (const h of doc.headings) {
|
|
2432
|
+
const indent = " ".repeat(Math.max(0, h.level - 1));
|
|
2433
|
+
console.log(` ${indent}${h.text} [${h.slug}]`);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
if (doc.links.length > 0) {
|
|
2437
|
+
console.log();
|
|
2438
|
+
console.log("Links:");
|
|
2439
|
+
for (const link of doc.links) {
|
|
2440
|
+
console.log(` ${link}`);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
function renderSection(doc) {
|
|
2445
|
+
console.log(`Path: ${doc.filepath}`);
|
|
2446
|
+
console.log(`Version: ${doc.version}`);
|
|
2447
|
+
console.log(`Heading: ${doc.heading.text} [${doc.heading.slug}] (H${doc.heading.level})`);
|
|
2448
|
+
if (doc.prevSlug) console.log(`Prev: ${doc.prevSlug}`);
|
|
2449
|
+
if (doc.nextSlug) console.log(`Next: ${doc.nextSlug}`);
|
|
2450
|
+
console.log("---");
|
|
2451
|
+
console.log(doc.content);
|
|
2452
|
+
}
|
|
2453
|
+
function renderFull(doc) {
|
|
2454
|
+
console.log(`Title: ${doc.title}`);
|
|
2455
|
+
console.log(`Path: ${doc.filepath}`);
|
|
2456
|
+
console.log(`Category: ${doc.category}`);
|
|
2457
|
+
console.log(`Tags: ${doc.tags.length > 0 ? doc.tags.join(", ") : "(none)"}`);
|
|
2458
|
+
if (doc.createdAt) console.log(`Created: ${doc.createdAt}`);
|
|
2459
|
+
if (doc.updatedAt) console.log(`Updated: ${doc.updatedAt}`);
|
|
2460
|
+
if (doc.metadata && Object.keys(doc.metadata).length > 0) {
|
|
2461
|
+
console.log("Metadata:");
|
|
2462
|
+
for (const [key, value] of Object.entries(doc.metadata)) {
|
|
2463
|
+
const rendered = value !== null && typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
2464
|
+
console.log(` ${key}: ${rendered}`);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
console.log("---");
|
|
2468
|
+
console.log(doc.content);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// src/commands/inbox.ts
|
|
2472
|
+
init_api_client();
|
|
2473
|
+
function formatItem(item) {
|
|
2474
|
+
const date = item.createdAt ? new Date(item.createdAt).toLocaleDateString() : "unknown";
|
|
2475
|
+
const tags = item.tags && item.tags.length > 0 ? item.tags.join(", ") : "(none)";
|
|
2476
|
+
return ` ${item.title}
|
|
2477
|
+
ID: ${item.documentId}
|
|
2478
|
+
Date: ${date}
|
|
2479
|
+
Tags: ${tags}`;
|
|
2480
|
+
}
|
|
2481
|
+
function registerInboxCommand(program2) {
|
|
2482
|
+
const inbox = program2.command("inbox").description("Manage inbox items");
|
|
2483
|
+
inbox.command("list").description("List all items in the Inbox").option("-l, --limit <number>", "Max items to show", "20").action(async (options) => {
|
|
2484
|
+
try {
|
|
2485
|
+
const client = new ApiClient();
|
|
2486
|
+
const params = new URLSearchParams({
|
|
2487
|
+
pathPrefix: "0-Inbox/",
|
|
2488
|
+
limit: options.limit
|
|
2489
|
+
});
|
|
2490
|
+
const data = await client.get(`/v1/documents?${params.toString()}`);
|
|
2491
|
+
if (data.items.length === 0) {
|
|
2492
|
+
console.log("Inbox is empty.");
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
for (const item of data.items) {
|
|
2496
|
+
console.log(formatItem(item));
|
|
2497
|
+
console.log();
|
|
2498
|
+
}
|
|
2499
|
+
console.log(`Total: ${data.items.length} item${data.items.length === 1 ? "" : "s"}`);
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2502
|
+
console.error(`Error: ${message}`);
|
|
2503
|
+
process.exitCode = 1;
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// src/commands/link.ts
|
|
2509
|
+
init_api_client();
|
|
2510
|
+
function registerLinkCommand(program2) {
|
|
2511
|
+
program2.command("link <docId> <relatedDocIds...>").description("Link a document to one or more related documents").action(async (docId, relatedDocIds) => {
|
|
2512
|
+
try {
|
|
2513
|
+
const client = new ApiClient();
|
|
2514
|
+
await client.post(`/v1/documents/${docId}/link`, { linkedDocIds: relatedDocIds });
|
|
2515
|
+
console.log(`Linked ${docId} to ${relatedDocIds.join(", ")}`);
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2518
|
+
console.error(`Error: ${message}`);
|
|
2519
|
+
process.exitCode = 1;
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// src/commands/list.ts
|
|
2525
|
+
import chalk3 from "chalk";
|
|
2526
|
+
import Table2 from "cli-table3";
|
|
2527
|
+
init_api_client();
|
|
2528
|
+
function registerListCommand(program2) {
|
|
2529
|
+
program2.command("list").description("List documents with optional filters").option("--path-prefix <prefix>", "Filter by path prefix").option("--filepath <path>", "Filter by exact filepath").option("--workspace <id>", "Workspace ID").option("--limit <n>", "Max results", "20").action(
|
|
2530
|
+
async (options) => {
|
|
2531
|
+
try {
|
|
2532
|
+
const params = new URLSearchParams();
|
|
2533
|
+
params.set("limit", options.limit);
|
|
2534
|
+
if (options.pathPrefix) {
|
|
2535
|
+
params.set("pathPrefix", options.pathPrefix);
|
|
2536
|
+
}
|
|
2537
|
+
if (options.filepath) {
|
|
2538
|
+
params.set("filepath", options.filepath);
|
|
2539
|
+
}
|
|
2540
|
+
if (options.workspace) {
|
|
2541
|
+
params.set("workspace", options.workspace);
|
|
2542
|
+
}
|
|
2543
|
+
const client = new ApiClient();
|
|
2544
|
+
const response = await client.get(
|
|
2545
|
+
`/v1/documents?${params.toString()}`
|
|
2546
|
+
);
|
|
2547
|
+
const docs = response.items ?? [];
|
|
2548
|
+
recordMeasurement(
|
|
2549
|
+
"list",
|
|
2550
|
+
{
|
|
2551
|
+
pathPrefix: options.pathPrefix,
|
|
2552
|
+
filepath: options.filepath,
|
|
2553
|
+
workspaceId: options.workspace,
|
|
2554
|
+
limit: options.limit
|
|
2555
|
+
},
|
|
2556
|
+
JSON.stringify(docs).length
|
|
2557
|
+
);
|
|
2558
|
+
if (docs.length === 0) {
|
|
2559
|
+
console.log(chalk3.yellow("No documents found."));
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
const table = new Table2({
|
|
2563
|
+
head: [chalk3.cyan("Title"), chalk3.cyan("Filepath"), chalk3.cyan("Document ID")],
|
|
2564
|
+
style: { head: [], border: ["gray"] }
|
|
2565
|
+
});
|
|
2566
|
+
for (const doc of docs) {
|
|
2567
|
+
table.push([doc.title, doc.filepath, doc.documentId]);
|
|
2568
|
+
}
|
|
2569
|
+
console.log(table.toString());
|
|
2570
|
+
console.log(chalk3.gray(`
|
|
2571
|
+
Total: ${docs.length} document(s)`));
|
|
2572
|
+
if (response.cursor) {
|
|
2573
|
+
console.log(chalk3.gray("More results available \u2014 re-run with a larger --limit."));
|
|
2574
|
+
}
|
|
2575
|
+
} catch (error) {
|
|
2576
|
+
exitWithError(error);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// src/commands/load.ts
|
|
2583
|
+
import chalk4 from "chalk";
|
|
2584
|
+
init_api_client();
|
|
2585
|
+
function normalize(s) {
|
|
2586
|
+
return s.toLowerCase().replace(/[-_\s]/g, "");
|
|
2587
|
+
}
|
|
2588
|
+
function resolveProjectName(input, projectNames) {
|
|
2589
|
+
let name = input;
|
|
2590
|
+
if (name.startsWith("1-Projects/")) {
|
|
2591
|
+
name = name.slice("1-Projects/".length);
|
|
2592
|
+
}
|
|
2593
|
+
name = name.replace(/\/+$/, "");
|
|
2594
|
+
if (projectNames.includes(name)) return name;
|
|
2595
|
+
const normalizedInput = normalize(name);
|
|
2596
|
+
for (const projectName of projectNames) {
|
|
2597
|
+
if (normalize(projectName) === normalizedInput) return projectName;
|
|
2598
|
+
}
|
|
2599
|
+
return null;
|
|
2600
|
+
}
|
|
2601
|
+
function registerLoadCommand(program2) {
|
|
2602
|
+
program2.command("load <project>").description("Load project context (AGENTS.md + frontmind + backmind listing)").option("--workspace <id>", "Workspace ID").option("--json", "Output raw JSON").action(async (project, options) => {
|
|
2603
|
+
try {
|
|
2604
|
+
const client = new ApiClient();
|
|
2605
|
+
const wsParam = options.workspace ? `?workspaceId=${options.workspace}` : "";
|
|
2606
|
+
const structure = await client.get(
|
|
2607
|
+
`/v1/workspace/structure${wsParam}`
|
|
2608
|
+
);
|
|
2609
|
+
const projectNames = structure.structure.projects;
|
|
2610
|
+
const matchedName = resolveProjectName(project, projectNames);
|
|
2611
|
+
if (!matchedName) {
|
|
2612
|
+
if (options.json) {
|
|
2613
|
+
console.log(
|
|
2614
|
+
JSON.stringify(
|
|
2615
|
+
{
|
|
2616
|
+
projectPath: null,
|
|
2617
|
+
match: "not_found",
|
|
2618
|
+
input: project,
|
|
2619
|
+
availableProjects: projectNames
|
|
2620
|
+
},
|
|
2621
|
+
null,
|
|
2622
|
+
2
|
|
2623
|
+
)
|
|
2624
|
+
);
|
|
2625
|
+
} else {
|
|
2626
|
+
console.error(chalk4.red(`Project not found: "${project}"`));
|
|
2627
|
+
console.log(chalk4.yellow(`Available projects: ${projectNames.join(", ") || "(none)"}`));
|
|
2628
|
+
}
|
|
2629
|
+
process.exitCode = 1;
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
const projectPath = `1-Projects/${matchedName}/`;
|
|
2633
|
+
const wsQuery = options.workspace ? `&workspace=${options.workspace}` : "";
|
|
2634
|
+
const [agentsMdRes, frontmindRes, backmindRes] = await Promise.allSettled([
|
|
2635
|
+
client.get(
|
|
2636
|
+
`/v1/documents?filepath=${encodeURIComponent(`${projectPath}AGENTS.md`)}${wsQuery}`
|
|
2637
|
+
),
|
|
2638
|
+
client.get(
|
|
2639
|
+
`/v1/documents?pathPrefix=${encodeURIComponent(`${projectPath}frontmind/`)}${wsQuery}&limit=200`
|
|
2640
|
+
),
|
|
2641
|
+
client.get(
|
|
2642
|
+
`/v1/documents?pathPrefix=${encodeURIComponent(`${projectPath}backmind/`)}${wsQuery}&limit=200`
|
|
2643
|
+
)
|
|
2644
|
+
]);
|
|
2645
|
+
const agentsMd = agentsMdRes.status === "fulfilled" && agentsMdRes.value.items?.length > 0 ? agentsMdRes.value.items[0] : null;
|
|
2646
|
+
const frontmind = frontmindRes.status === "fulfilled" ? frontmindRes.value.items ?? [] : [];
|
|
2647
|
+
const backmind = backmindRes.status === "fulfilled" ? (backmindRes.value.items ?? []).map((d) => ({
|
|
2648
|
+
documentId: d.documentId,
|
|
2649
|
+
filepath: d.filepath,
|
|
2650
|
+
title: d.title
|
|
2651
|
+
})) : [];
|
|
2652
|
+
const charsReturned = (agentsMd?.content?.length ?? 0) + frontmind.reduce((s, d) => s + (d.content?.length ?? 0), 0) + JSON.stringify(backmind).length;
|
|
2653
|
+
recordMeasurement(
|
|
2654
|
+
"load",
|
|
2655
|
+
{
|
|
2656
|
+
projectPath,
|
|
2657
|
+
workspaceId: options.workspace,
|
|
2658
|
+
json: options.json ?? false
|
|
2659
|
+
},
|
|
2660
|
+
charsReturned
|
|
2661
|
+
);
|
|
2662
|
+
if (options.json) {
|
|
2663
|
+
console.log(
|
|
2664
|
+
JSON.stringify(
|
|
2665
|
+
{
|
|
2666
|
+
projectPath,
|
|
2667
|
+
agentsMd,
|
|
2668
|
+
frontmind,
|
|
2669
|
+
backmind,
|
|
2670
|
+
availableProjects: projectNames
|
|
2671
|
+
},
|
|
2672
|
+
null,
|
|
2673
|
+
2
|
|
2674
|
+
)
|
|
2675
|
+
);
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
console.log(chalk4.bold.cyan(`
|
|
2679
|
+
=== ${matchedName} ===
|
|
2680
|
+
`));
|
|
2681
|
+
if (agentsMd) {
|
|
2682
|
+
console.log(chalk4.bold("AGENTS.md:"));
|
|
2683
|
+
console.log(agentsMd.content);
|
|
2684
|
+
console.log();
|
|
2685
|
+
} else {
|
|
2686
|
+
console.log(chalk4.yellow("No AGENTS.md found for this project.\n"));
|
|
2687
|
+
}
|
|
2688
|
+
if (frontmind.length > 0) {
|
|
2689
|
+
console.log(chalk4.bold(`Frontmind (${frontmind.length} files):`));
|
|
2690
|
+
for (const doc of frontmind) {
|
|
2691
|
+
console.log(chalk4.cyan(`
|
|
2692
|
+
--- ${doc.filepath} ---`));
|
|
2693
|
+
console.log(doc.content);
|
|
2694
|
+
}
|
|
2695
|
+
console.log();
|
|
2696
|
+
} else {
|
|
2697
|
+
console.log(chalk4.gray("No frontmind files.\n"));
|
|
2698
|
+
}
|
|
2699
|
+
if (backmind.length > 0) {
|
|
2700
|
+
console.log(chalk4.bold(`Backmind (${backmind.length} files available):`));
|
|
2701
|
+
for (const entry of backmind) {
|
|
2702
|
+
console.log(chalk4.gray(` ${entry.filepath} \u2014 ${entry.title}`));
|
|
2703
|
+
}
|
|
2704
|
+
console.log(chalk4.gray("\n Use: bisque get <documentId> to load any backmind file."));
|
|
2705
|
+
} else {
|
|
2706
|
+
console.log(chalk4.gray("No backmind files."));
|
|
2707
|
+
}
|
|
2708
|
+
} catch (error) {
|
|
2709
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2710
|
+
console.error(`Error: ${message}`);
|
|
2711
|
+
process.exitCode = 1;
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// src/commands/ls.ts
|
|
2717
|
+
init_api_client();
|
|
2718
|
+
import chalk5 from "chalk";
|
|
2719
|
+
function formatFolder(entry) {
|
|
2720
|
+
const countInfo = entry.childCount != null ? `(${entry.childCount} files)` : entry.hasChildren ? "(has children)" : "";
|
|
2721
|
+
return ` ${chalk5.blue(`${entry.name}/`)} ${chalk5.dim(countInfo)}`;
|
|
2722
|
+
}
|
|
2723
|
+
function formatFile(entry) {
|
|
2724
|
+
const date = entry.updatedAt ? new Date(entry.updatedAt).toISOString().split("T")[0] : "";
|
|
2725
|
+
return ` ${entry.name} ${chalk5.dim(`"${entry.title}"`)} ${chalk5.dim(date)}`;
|
|
2726
|
+
}
|
|
2727
|
+
function buildParams(path2, options) {
|
|
2728
|
+
const params = new URLSearchParams();
|
|
2729
|
+
if (path2) params.set("path", path2);
|
|
2730
|
+
params.set("workspace", options.workspace);
|
|
2731
|
+
params.set("limit", options.limit);
|
|
2732
|
+
if (options.counts) params.set("counts", "true");
|
|
2733
|
+
return params;
|
|
2734
|
+
}
|
|
2735
|
+
function displayEntries(entries) {
|
|
2736
|
+
for (const entry of entries) {
|
|
2737
|
+
console.log(entry.type === "folder" ? formatFolder(entry) : formatFile(entry));
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
function registerLsCommand(program2) {
|
|
2741
|
+
program2.command("ls [path]").description("List contents of a directory in Bisque workspace").option("--workspace <id>", "Workspace ID", "default").option("--counts", "Show exact child counts for folders").option("--json", "Output as JSON").option("--limit <n>", "Max entries", "50").action(async (path2, options) => {
|
|
2742
|
+
try {
|
|
2743
|
+
const client = new ApiClient();
|
|
2744
|
+
const params = buildParams(path2, options);
|
|
2745
|
+
const result = await client.get(`/v1/documents/ls?${params.toString()}`);
|
|
2746
|
+
if (options.json) {
|
|
2747
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
const data = result;
|
|
2751
|
+
if (!data.entries || data.entries.length === 0) {
|
|
2752
|
+
console.log(chalk5.dim(" (empty)"));
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
displayEntries(data.entries);
|
|
2756
|
+
} catch (err) {
|
|
2757
|
+
exitWithError(err);
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// src/commands/maintenance.ts
|
|
2763
|
+
init_api_client();
|
|
2764
|
+
import chalk6 from "chalk";
|
|
2765
|
+
function healthColor(health) {
|
|
2766
|
+
switch (health) {
|
|
2767
|
+
case "healthy":
|
|
2768
|
+
return chalk6.green(health);
|
|
2769
|
+
case "needs-attention":
|
|
2770
|
+
return chalk6.yellow(health);
|
|
2771
|
+
case "needs-maintenance":
|
|
2772
|
+
return chalk6.red(health);
|
|
2773
|
+
default:
|
|
2774
|
+
return health;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
function scopeLabel(scope) {
|
|
2778
|
+
if (scope.filter) {
|
|
2779
|
+
return `${scope.type}: ${scope.filter}`;
|
|
2780
|
+
}
|
|
2781
|
+
return scope.type;
|
|
2782
|
+
}
|
|
2783
|
+
function printRecommendationLine(label, count) {
|
|
2784
|
+
if (count > 0) {
|
|
2785
|
+
console.log(` ${chalk6.yellow("*")} ${label}: ${count}`);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
function registerMaintenanceCommand(program2) {
|
|
2789
|
+
program2.command("maintenance").description("Run workspace maintenance and get health report").option("--project <path>", "Scope to a specific project path").option("--category <category>", "Scope to a PARA category").action(async (options) => {
|
|
2790
|
+
try {
|
|
2791
|
+
const client = new ApiClient();
|
|
2792
|
+
const body = {};
|
|
2793
|
+
if (options.project) {
|
|
2794
|
+
body["scope"] = "project";
|
|
2795
|
+
body["filter"] = options.project;
|
|
2796
|
+
} else if (options.category) {
|
|
2797
|
+
body["scope"] = "category";
|
|
2798
|
+
body["filter"] = options.category;
|
|
2799
|
+
} else {
|
|
2800
|
+
body["scope"] = "workspace";
|
|
2801
|
+
}
|
|
2802
|
+
const report = await client.post("/v1/maintenance", body);
|
|
2803
|
+
console.log();
|
|
2804
|
+
console.log(chalk6.bold("Workspace Maintenance Report"));
|
|
2805
|
+
console.log(`Scope: ${scopeLabel(report.scope)} | ${report.timestamp}`);
|
|
2806
|
+
console.log();
|
|
2807
|
+
const { mechanical } = report;
|
|
2808
|
+
const hasMechanical = mechanical.taskLocksReleased.length > 0 || mechanical.chainsCompleted.length > 0 || mechanical.errors.length > 0;
|
|
2809
|
+
if (hasMechanical) {
|
|
2810
|
+
console.log(chalk6.bold("Mechanical Fixes"));
|
|
2811
|
+
for (const lock of mechanical.taskLocksReleased) {
|
|
2812
|
+
console.log(
|
|
2813
|
+
` ${chalk6.green("\u2713")} Released expired lock: ${lock.title} (${lock.taskId.substring(0, 8)})`
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
for (const chain of mechanical.chainsCompleted) {
|
|
2817
|
+
console.log(
|
|
2818
|
+
` ${chalk6.green("\u2713")} Completed chain: ${chain.name} (${chain.chainId.substring(0, 8)})`
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
for (const err of mechanical.errors) {
|
|
2822
|
+
console.log(
|
|
2823
|
+
` ${chalk6.red("\u2717")} ${err.operation} failed on ${err.entityId.substring(0, 8)}: ${err.error}`
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
console.log();
|
|
2827
|
+
}
|
|
2828
|
+
const { recommendations: rec } = report;
|
|
2829
|
+
const hasRecommendations = report.summary.recommendations > 0;
|
|
2830
|
+
if (hasRecommendations) {
|
|
2831
|
+
console.log(chalk6.bold("Recommendations"));
|
|
2832
|
+
printRecommendationLine("Inbox items pending triage", rec.inboxItems.length);
|
|
2833
|
+
printRecommendationLine("Stale documents", rec.staleDocs.length);
|
|
2834
|
+
printRecommendationLine("AGENTS.md refresh needed", rec.agentsMdRefreshNeeded.length);
|
|
2835
|
+
printRecommendationLine("Frontmind stale", rec.frontmindStale.length);
|
|
2836
|
+
printRecommendationLine("Frontmind missing", rec.frontmindMissing.length);
|
|
2837
|
+
printRecommendationLine(
|
|
2838
|
+
"Archive distillation candidates",
|
|
2839
|
+
rec.archiveDistillation.length
|
|
2840
|
+
);
|
|
2841
|
+
printRecommendationLine("Ready tasks", rec.readyTasks.length);
|
|
2842
|
+
printRecommendationLine("Orphaned tasks", rec.orphanedTasks.length);
|
|
2843
|
+
printRecommendationLine("Broken links", rec.brokenLinks.length);
|
|
2844
|
+
printRecommendationLine("Misplaced documents", rec.misplacedDocs.length);
|
|
2845
|
+
console.log();
|
|
2846
|
+
}
|
|
2847
|
+
console.log(chalk6.bold("Summary"));
|
|
2848
|
+
console.log(` Mechanical fixes: ${report.summary.mechanicalFixes}`);
|
|
2849
|
+
console.log(` Recommendations: ${report.summary.recommendations}`);
|
|
2850
|
+
console.log(` Health: ${healthColor(report.summary.workspaceHealth)}`);
|
|
2851
|
+
console.log();
|
|
2852
|
+
} catch (error) {
|
|
2853
|
+
exitWithError(error);
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
// src/commands/manifest.ts
|
|
2859
|
+
init_api_client();
|
|
2860
|
+
function registerManifestCommand(program2) {
|
|
2861
|
+
program2.command("manifest").description("Get workspace summary data for manifest generation (BISQUE.md)").action(async () => {
|
|
2862
|
+
try {
|
|
2863
|
+
const client = new ApiClient();
|
|
2864
|
+
const summary = await client.get("/v1/workspace/summary");
|
|
2865
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2868
|
+
console.error(`Error: ${message}`);
|
|
2869
|
+
process.exitCode = 1;
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// src/commands/measure.ts
|
|
2875
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2876
|
+
function parseReferencesFlag(input) {
|
|
2877
|
+
const refs = [];
|
|
2878
|
+
for (const part of input.split(",")) {
|
|
2879
|
+
const trimmed = part.trim();
|
|
2880
|
+
if (!trimmed) continue;
|
|
2881
|
+
const idx = trimmed.lastIndexOf(":");
|
|
2882
|
+
if (idx < 0) continue;
|
|
2883
|
+
const path2 = trimmed.slice(0, idx).trim();
|
|
2884
|
+
const chars = Number.parseInt(trimmed.slice(idx + 1).trim(), 10);
|
|
2885
|
+
if (!path2 || Number.isNaN(chars)) continue;
|
|
2886
|
+
refs.push({ path: path2, chars });
|
|
2887
|
+
}
|
|
2888
|
+
return refs;
|
|
2889
|
+
}
|
|
2890
|
+
function pct(sorted, p) {
|
|
2891
|
+
if (sorted.length === 0) return 0;
|
|
2892
|
+
const idx = Math.min(sorted.length - 1, Math.floor(sorted.length * p));
|
|
2893
|
+
return sorted[idx] ?? 0;
|
|
2894
|
+
}
|
|
2895
|
+
function summarize(rows, opts = {}) {
|
|
2896
|
+
const sinceMs = opts.since ? Date.parse(opts.since) : Number.NaN;
|
|
2897
|
+
const filtered = rows.filter((r) => {
|
|
2898
|
+
if (opts.by && r.command !== opts.by) return false;
|
|
2899
|
+
if (!Number.isNaN(sinceMs) && Date.parse(r.ts) < sinceMs) return false;
|
|
2900
|
+
return true;
|
|
2901
|
+
});
|
|
2902
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2903
|
+
for (const r of filtered) {
|
|
2904
|
+
const arr = groups.get(r.command) ?? [];
|
|
2905
|
+
arr.push(r.tokensEstimate);
|
|
2906
|
+
groups.set(r.command, arr);
|
|
2907
|
+
}
|
|
2908
|
+
const stats = [];
|
|
2909
|
+
for (const [command, tokens] of groups) {
|
|
2910
|
+
const sorted = [...tokens].sort((a, b) => a - b);
|
|
2911
|
+
const total = sorted.reduce((s, n) => s + n, 0);
|
|
2912
|
+
stats.push({
|
|
2913
|
+
command,
|
|
2914
|
+
n: sorted.length,
|
|
2915
|
+
avg: Math.round(total / sorted.length),
|
|
2916
|
+
p50: pct(sorted, 0.5),
|
|
2917
|
+
p95: pct(sorted, 0.95),
|
|
2918
|
+
total
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
stats.sort((a, b) => b.total - a.total);
|
|
2922
|
+
return { stats, filteredCount: filtered.length };
|
|
2923
|
+
}
|
|
2924
|
+
function summarizeReferences(rows, opts = {}) {
|
|
2925
|
+
const sinceMs = opts.since ? Date.parse(opts.since) : Number.NaN;
|
|
2926
|
+
const filtered = rows.filter((r) => {
|
|
2927
|
+
if (r.command !== "references") return false;
|
|
2928
|
+
if (!Number.isNaN(sinceMs) && Date.parse(r.ts) < sinceMs) return false;
|
|
2929
|
+
return true;
|
|
2930
|
+
});
|
|
2931
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2932
|
+
for (const r of filtered) {
|
|
2933
|
+
const refs = r.args?.["references"] ?? [];
|
|
2934
|
+
for (const ref of refs) {
|
|
2935
|
+
if (!ref || typeof ref.path !== "string") continue;
|
|
2936
|
+
const tokens = Math.ceil((ref.chars ?? 0) / 4);
|
|
2937
|
+
const g = groups.get(ref.path) ?? { tokens: [], lastLoaded: r.ts };
|
|
2938
|
+
g.tokens.push(tokens);
|
|
2939
|
+
if (r.ts > g.lastLoaded) g.lastLoaded = r.ts;
|
|
2940
|
+
groups.set(ref.path, g);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
const stats = [];
|
|
2944
|
+
for (const [path2, g] of groups) {
|
|
2945
|
+
const sorted = [...g.tokens].sort((a, b) => a - b);
|
|
2946
|
+
const total = sorted.reduce((s, n) => s + n, 0);
|
|
2947
|
+
stats.push({
|
|
2948
|
+
path: path2,
|
|
2949
|
+
n: sorted.length,
|
|
2950
|
+
avg: Math.round(total / sorted.length),
|
|
2951
|
+
p50: pct(sorted, 0.5),
|
|
2952
|
+
p95: pct(sorted, 0.95),
|
|
2953
|
+
total,
|
|
2954
|
+
lastLoaded: g.lastLoaded
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
stats.sort((a, b) => b.total - a.total);
|
|
2958
|
+
return { stats, filteredCount: filtered.length };
|
|
2959
|
+
}
|
|
2960
|
+
function renderReferenceTable(stats) {
|
|
2961
|
+
const header = "| path | n loads | avg tokens | p50 | p95 | total tokens | last loaded |";
|
|
2962
|
+
const sep = "|---|---|---|---|---|---|---|";
|
|
2963
|
+
const body = stats.map(
|
|
2964
|
+
(s) => `| ${s.path} | ${s.n} | ${s.avg} | ${s.p50} | ${s.p95} | ${s.total} | ${s.lastLoaded} |`
|
|
2965
|
+
);
|
|
2966
|
+
return [header, sep, ...body].join("\n");
|
|
2967
|
+
}
|
|
2968
|
+
function renderTable(stats) {
|
|
2969
|
+
const header = "| command | n calls | avg tokens | p50 | p95 | total tokens |";
|
|
2970
|
+
const sep = "|---|---|---|---|---|---|";
|
|
2971
|
+
const body = stats.map(
|
|
2972
|
+
(s) => `| ${s.command} | ${s.n} | ${s.avg} | ${s.p50} | ${s.p95} | ${s.total} |`
|
|
2973
|
+
);
|
|
2974
|
+
return [header, sep, ...body].join("\n");
|
|
2975
|
+
}
|
|
2976
|
+
function loadRows() {
|
|
2977
|
+
const path2 = measurementsPath();
|
|
2978
|
+
if (!existsSync(path2)) return null;
|
|
2979
|
+
const raw = readFileSync(path2, "utf-8");
|
|
2980
|
+
const rows = [];
|
|
2981
|
+
for (const line of raw.split("\n")) {
|
|
2982
|
+
if (!line.trim()) continue;
|
|
2983
|
+
try {
|
|
2984
|
+
rows.push(JSON.parse(line));
|
|
2985
|
+
} catch {
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return rows;
|
|
2989
|
+
}
|
|
2990
|
+
function runSummarize(opts) {
|
|
2991
|
+
const rows = loadRows();
|
|
2992
|
+
if (rows === null) {
|
|
2993
|
+
console.log("No measurements yet. Set BISQUE_MEASURE=1 and run some commands first.");
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
if (opts.references) {
|
|
2997
|
+
const { stats: stats2, filteredCount: filteredCount2 } = summarizeReferences(rows, { since: opts.since });
|
|
2998
|
+
if (filteredCount2 === 0) {
|
|
2999
|
+
console.log("No `references` measurements match your filters.");
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
console.log(renderReferenceTable(stats2));
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
const { stats, filteredCount } = summarize(rows, opts);
|
|
3006
|
+
if (filteredCount === 0) {
|
|
3007
|
+
console.log("No measurements match your filters.");
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
console.log(renderTable(stats));
|
|
3011
|
+
}
|
|
3012
|
+
function registerMeasureCommand(program2) {
|
|
3013
|
+
const measure = program2.command("measure").description("Inspect measurement data captured by BISQUE_MEASURE=1");
|
|
3014
|
+
measure.command("summarize").description("Summarize measurements.jsonl as a markdown table").option("--since <iso>", "Filter rows with ts >= this ISO date").option("--by <command>", "Filter to a single command name").option("--references", "Roll up per-reference-doc load stats from `references` events").action(runSummarize);
|
|
3015
|
+
measure.command("references").description("Record a subagent reference-doc load event (chain-runner telemetry)").requiredOption("--task-id <id>", "Task ID being dispatched").requiredOption(
|
|
3016
|
+
"--slash-command <name>",
|
|
3017
|
+
"Slash command building the envelope (e.g. run-chain)"
|
|
3018
|
+
).requiredOption(
|
|
3019
|
+
"--references <list>",
|
|
3020
|
+
"Comma-separated path:chars pairs (e.g. 'references/chain-runner.md:12000,references/task-workflow.md:8000')"
|
|
3021
|
+
).option(
|
|
3022
|
+
"--preamble-chars <n>",
|
|
3023
|
+
"Total envelope char count, including preamble",
|
|
3024
|
+
(v) => Number.parseInt(v, 10)
|
|
3025
|
+
).action(
|
|
3026
|
+
(opts) => {
|
|
3027
|
+
const refs = parseReferencesFlag(opts.references);
|
|
3028
|
+
const totalChars = refs.reduce((s, r) => s + r.chars, 0);
|
|
3029
|
+
recordMeasurement(
|
|
3030
|
+
"references",
|
|
3031
|
+
{
|
|
3032
|
+
taskId: opts.taskId,
|
|
3033
|
+
slashCommand: opts.slashCommand,
|
|
3034
|
+
references: refs,
|
|
3035
|
+
preambleChars: opts.preambleChars
|
|
3036
|
+
},
|
|
3037
|
+
totalChars
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
// src/commands/move.ts
|
|
3044
|
+
init_api_client();
|
|
3045
|
+
function registerMoveCommand(program2) {
|
|
3046
|
+
program2.command("move <docId>").description("Move a document to a new path").requiredOption("--to <path>", "Destination path").action(async (docId, options) => {
|
|
3047
|
+
try {
|
|
3048
|
+
const client = new ApiClient();
|
|
3049
|
+
await client.put(`/v1/documents/${docId}/move`, { newPath: options.to });
|
|
3050
|
+
console.log(`Moved ${docId} \u2192 ${options.to}`);
|
|
3051
|
+
} catch (error) {
|
|
3052
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3053
|
+
console.error(`Error: ${message}`);
|
|
3054
|
+
process.exitCode = 1;
|
|
3055
|
+
}
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// src/commands/persona.ts
|
|
3060
|
+
init_api_client();
|
|
3061
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
3062
|
+
import chalk7 from "chalk";
|
|
3063
|
+
import Table3 from "cli-table3";
|
|
3064
|
+
function confirm2(prompt) {
|
|
3065
|
+
const rl = createInterface2({
|
|
3066
|
+
input: process.stdin,
|
|
3067
|
+
output: process.stdout
|
|
3068
|
+
});
|
|
3069
|
+
return new Promise((resolve) => {
|
|
3070
|
+
rl.question(prompt, (answer) => {
|
|
3071
|
+
rl.close();
|
|
3072
|
+
resolve(answer.toLowerCase() === "y");
|
|
3073
|
+
});
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
function registerPersonaCommand(program2) {
|
|
3077
|
+
const persona = program2.command("persona").description("Manage agent personas");
|
|
3078
|
+
persona.command("list").description("List all personas").option("--json", "Output raw JSON").action(async (options) => {
|
|
3079
|
+
try {
|
|
3080
|
+
const client = new ApiClient();
|
|
3081
|
+
const data = await client.get("/v1/agent-personas");
|
|
3082
|
+
if (options.json) {
|
|
3083
|
+
console.log(JSON.stringify(data, null, 2));
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
if (data.agentPersonas.length === 0) {
|
|
3087
|
+
console.log(chalk7.yellow("No personas found."));
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
const table = new Table3({
|
|
3091
|
+
head: [
|
|
3092
|
+
chalk7.cyan("ID"),
|
|
3093
|
+
chalk7.cyan("Name"),
|
|
3094
|
+
chalk7.cyan("Specialization"),
|
|
3095
|
+
chalk7.cyan("Tasks"),
|
|
3096
|
+
chalk7.cyan("Last Active")
|
|
3097
|
+
],
|
|
3098
|
+
style: {
|
|
3099
|
+
head: [],
|
|
3100
|
+
border: ["gray"]
|
|
3101
|
+
}
|
|
3102
|
+
});
|
|
3103
|
+
for (const item of data.agentPersonas) {
|
|
3104
|
+
const spec = item.specialization.length > 30 ? `${item.specialization.slice(0, 29)}...` : item.specialization;
|
|
3105
|
+
table.push([
|
|
3106
|
+
item.agentPersonaId,
|
|
3107
|
+
item.name,
|
|
3108
|
+
spec,
|
|
3109
|
+
String(item.tasksCompleted ?? 0),
|
|
3110
|
+
item.lastActive ? item.lastActive.split("T")[0] : "-"
|
|
3111
|
+
]);
|
|
3112
|
+
}
|
|
3113
|
+
console.log(table.toString());
|
|
3114
|
+
console.log(chalk7.gray(`
|
|
3115
|
+
Total: ${data.agentPersonas.length} persona(s)`));
|
|
3116
|
+
} catch (error) {
|
|
3117
|
+
exitWithError(error);
|
|
3118
|
+
}
|
|
3119
|
+
});
|
|
3120
|
+
persona.command("get <id>").description("Get a persona by ID").action(async (id) => {
|
|
3121
|
+
try {
|
|
3122
|
+
const client = new ApiClient();
|
|
3123
|
+
const p = await client.get(`/v1/agent-personas/${id}`);
|
|
3124
|
+
console.log(`Name: ${p.name}`);
|
|
3125
|
+
console.log(`ID: ${p.agentPersonaId}`);
|
|
3126
|
+
console.log(`Specialization: ${p.specialization}`);
|
|
3127
|
+
if (p.personality) console.log(`Personality: ${p.personality}`);
|
|
3128
|
+
if (p.coreValues) console.log(`Core Values: ${p.coreValues}`);
|
|
3129
|
+
if (p.reviewCriteria) console.log(`Review Criteria:${p.reviewCriteria}`);
|
|
3130
|
+
console.log(`Tasks Done: ${p.tasksCompleted}`);
|
|
3131
|
+
if (p.lastActive) console.log(`Last Active: ${p.lastActive}`);
|
|
3132
|
+
console.log(`Created: ${p.createdAt}`);
|
|
3133
|
+
console.log(`Updated: ${p.updatedAt}`);
|
|
3134
|
+
if (p.learnings && p.learnings.length > 0) {
|
|
3135
|
+
console.log(chalk7.cyan("\nLearnings:"));
|
|
3136
|
+
for (const l of p.learnings) {
|
|
3137
|
+
console.log(` - ${l}`);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
if (p.feedback && p.feedback.length > 0) {
|
|
3141
|
+
console.log(chalk7.cyan("\nFeedback:"));
|
|
3142
|
+
for (const f of p.feedback) {
|
|
3143
|
+
console.log(` - ${f}`);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
} catch (error) {
|
|
3147
|
+
exitWithError(error);
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
persona.command("create").description("Create a new persona").requiredOption("--name <name>", "Persona name").requiredOption("--specialization <spec>", "Specialization description").option("--personality <text>", "Personality description").option("--core-values <text>", "Core values").option("--review-criteria <text>", "Review criteria").action(
|
|
3151
|
+
async (options) => {
|
|
3152
|
+
try {
|
|
3153
|
+
const client = new ApiClient();
|
|
3154
|
+
const body = Object.assign(
|
|
3155
|
+
{
|
|
3156
|
+
name: options.name,
|
|
3157
|
+
specialization: options.specialization
|
|
3158
|
+
},
|
|
3159
|
+
options.personality ? { personality: options.personality } : {},
|
|
3160
|
+
options.coreValues ? { coreValues: options.coreValues } : {},
|
|
3161
|
+
options.reviewCriteria ? { reviewCriteria: options.reviewCriteria } : {}
|
|
3162
|
+
);
|
|
3163
|
+
const result = await client.post("/v1/agent-personas", body);
|
|
3164
|
+
console.log(`Persona created: ${result.agentPersonaId}`);
|
|
3165
|
+
} catch (error) {
|
|
3166
|
+
exitWithError(error);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
);
|
|
3170
|
+
persona.command("update <id>").description("Update a persona").option("--name <name>", "New name").option("--specialization <spec>", "New specialization").option("--personality <text>", "New personality").option("--core-values <text>", "New core values").option("--review-criteria <text>", "New review criteria").action(
|
|
3171
|
+
async (id, options) => {
|
|
3172
|
+
try {
|
|
3173
|
+
const body = Object.assign(
|
|
3174
|
+
{},
|
|
3175
|
+
options.name ? { name: options.name } : {},
|
|
3176
|
+
options.specialization ? { specialization: options.specialization } : {},
|
|
3177
|
+
options.personality ? { personality: options.personality } : {},
|
|
3178
|
+
options.coreValues ? { coreValues: options.coreValues } : {},
|
|
3179
|
+
options.reviewCriteria ? { reviewCriteria: options.reviewCriteria } : {}
|
|
3180
|
+
);
|
|
3181
|
+
if (Object.keys(body).length === 0) {
|
|
3182
|
+
console.error("Error: Provide at least one field to update.");
|
|
3183
|
+
process.exitCode = 1;
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
const client = new ApiClient();
|
|
3187
|
+
const result = await client.put(`/v1/agent-personas/${id}`, body);
|
|
3188
|
+
console.log(`Persona ${result.agentPersonaId} updated.`);
|
|
3189
|
+
} catch (error) {
|
|
3190
|
+
exitWithError(error);
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
);
|
|
3194
|
+
persona.command("delete <id>").description("Delete a persona").option("-f, --force", "Skip confirmation prompt").action(async (id, options) => {
|
|
3195
|
+
try {
|
|
3196
|
+
if (!options.force) {
|
|
3197
|
+
const confirmed = await confirm2(`Delete persona ${id}? (y/N) `);
|
|
3198
|
+
if (!confirmed) {
|
|
3199
|
+
console.log("Cancelled.");
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
const client = new ApiClient();
|
|
3204
|
+
await client.delete(`/v1/agent-personas/${id}`);
|
|
3205
|
+
console.log(`Persona ${id} deleted.`);
|
|
3206
|
+
} catch (error) {
|
|
3207
|
+
exitWithError(error);
|
|
3208
|
+
}
|
|
3209
|
+
});
|
|
3210
|
+
persona.command("add-learning <id> <text>").description("Add a learning to a persona").action(async (id, text) => {
|
|
3211
|
+
try {
|
|
3212
|
+
const client = new ApiClient();
|
|
3213
|
+
await client.post(`/v1/agent-personas/${id}/learnings`, { learning: text });
|
|
3214
|
+
console.log("Learning added.");
|
|
3215
|
+
} catch (error) {
|
|
3216
|
+
exitWithError(error);
|
|
3217
|
+
}
|
|
3218
|
+
});
|
|
3219
|
+
persona.command("add-feedback <id> <text>").description("Add feedback to a persona").action(async (id, text) => {
|
|
3220
|
+
try {
|
|
3221
|
+
const client = new ApiClient();
|
|
3222
|
+
await client.post(`/v1/agent-personas/${id}/feedback`, { feedback: text });
|
|
3223
|
+
console.log("Feedback added.");
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
exitWithError(error);
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// src/commands/plugin.ts
|
|
3231
|
+
import { spawn } from "node:child_process";
|
|
3232
|
+
var BISQUE_MARKETPLACE_SOURCE = "Kokonaut/bisque";
|
|
3233
|
+
var BISQUE_MARKETPLACE_NAME = "bisque-marketplace";
|
|
3234
|
+
var BISQUE_PLUGIN_NAME = "bisque";
|
|
3235
|
+
var BISQUE_PLUGIN_QUALIFIED = `${BISQUE_PLUGIN_NAME}@${BISQUE_MARKETPLACE_NAME}`;
|
|
3236
|
+
var CLAUDE_INSTALL_DOCS_URL = "https://docs.anthropic.com/en/docs/claude-code/setup";
|
|
3237
|
+
var defaultRunner = {
|
|
3238
|
+
run(command, args) {
|
|
3239
|
+
return new Promise((resolve, reject) => {
|
|
3240
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3241
|
+
let stdout = "";
|
|
3242
|
+
let stderr = "";
|
|
3243
|
+
child.stdout.on("data", (chunk) => {
|
|
3244
|
+
stdout += chunk.toString();
|
|
3245
|
+
});
|
|
3246
|
+
child.stderr.on("data", (chunk) => {
|
|
3247
|
+
stderr += chunk.toString();
|
|
3248
|
+
});
|
|
3249
|
+
child.on("error", (err) => reject(err));
|
|
3250
|
+
child.on("close", (code) => {
|
|
3251
|
+
resolve({ exitCode: code ?? 0, stdout, stderr });
|
|
3252
|
+
});
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
};
|
|
3256
|
+
function entryMatchesBisque(entry) {
|
|
3257
|
+
if (!entry || typeof entry !== "object") return false;
|
|
3258
|
+
if (entry.id === BISQUE_PLUGIN_QUALIFIED) return true;
|
|
3259
|
+
if (entry.name === BISQUE_PLUGIN_QUALIFIED) return true;
|
|
3260
|
+
if (entry.name === BISQUE_PLUGIN_NAME) {
|
|
3261
|
+
if (entry.marketplaceName === BISQUE_MARKETPLACE_NAME) return true;
|
|
3262
|
+
if (entry.source?.name === BISQUE_MARKETPLACE_NAME) return true;
|
|
3263
|
+
}
|
|
3264
|
+
return false;
|
|
3265
|
+
}
|
|
3266
|
+
async function isBisquePluginInstalled(runner) {
|
|
3267
|
+
try {
|
|
3268
|
+
const result = await runner.run("claude", ["plugin", "list", "--json"]);
|
|
3269
|
+
if (result.exitCode !== 0) return false;
|
|
3270
|
+
const trimmed = result.stdout.trim();
|
|
3271
|
+
if (!trimmed) return false;
|
|
3272
|
+
const parsed = JSON.parse(trimmed);
|
|
3273
|
+
const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed.installed) ? parsed.installed ?? [] : [];
|
|
3274
|
+
return entries.some(entryMatchesBisque);
|
|
3275
|
+
} catch {
|
|
3276
|
+
return false;
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
async function ensureClaudeOnPath(runner) {
|
|
3280
|
+
try {
|
|
3281
|
+
const result = await runner.run("claude", ["--version"]);
|
|
3282
|
+
return result.exitCode === 0;
|
|
3283
|
+
} catch (err) {
|
|
3284
|
+
const code = err.code;
|
|
3285
|
+
if (code === "ENOENT") return false;
|
|
3286
|
+
throw err;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
async function runPluginInstall(options = {}, deps = {}) {
|
|
3290
|
+
const runner = deps.runner ?? defaultRunner;
|
|
3291
|
+
const logger = deps.logger ?? console;
|
|
3292
|
+
const claudeAvailable = await ensureClaudeOnPath(runner);
|
|
3293
|
+
if (!claudeAvailable) {
|
|
3294
|
+
logger.error(
|
|
3295
|
+
`Error: Claude Code CLI ('claude') was not found on PATH.
|
|
3296
|
+
The Bisque plugin requires Claude Code to be installed first.
|
|
3297
|
+
Install Claude Code: ${CLAUDE_INSTALL_DOCS_URL}
|
|
3298
|
+
After installing, re-run \`bisque plugin install\`.`
|
|
3299
|
+
);
|
|
3300
|
+
return 1;
|
|
3301
|
+
}
|
|
3302
|
+
const alreadyInstalled = await isBisquePluginInstalled(runner);
|
|
3303
|
+
if (alreadyInstalled && !options.force) {
|
|
3304
|
+
logger.log(
|
|
3305
|
+
`Bisque plugin already installed as '${BISQUE_PLUGIN_QUALIFIED}'.
|
|
3306
|
+
Use --force to reinstall.`
|
|
3307
|
+
);
|
|
3308
|
+
return 0;
|
|
3309
|
+
}
|
|
3310
|
+
if (alreadyInstalled && options.force) {
|
|
3311
|
+
logger.log(`Reinstalling \u2014 removing existing '${BISQUE_PLUGIN_QUALIFIED}'...`);
|
|
3312
|
+
const uninstall = await runner.run("claude", ["plugin", "uninstall", BISQUE_PLUGIN_QUALIFIED]);
|
|
3313
|
+
if (uninstall.exitCode !== 0) {
|
|
3314
|
+
logger.error(
|
|
3315
|
+
`Error: 'claude plugin uninstall' failed (exit ${uninstall.exitCode}).
|
|
3316
|
+
stderr: ${uninstall.stderr.trim()}`
|
|
3317
|
+
);
|
|
3318
|
+
return 1;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
logger.log(`Adding marketplace '${BISQUE_MARKETPLACE_SOURCE}'...`);
|
|
3322
|
+
const addMarketplace = await runner.run("claude", [
|
|
3323
|
+
"plugin",
|
|
3324
|
+
"marketplace",
|
|
3325
|
+
"add",
|
|
3326
|
+
BISQUE_MARKETPLACE_SOURCE
|
|
3327
|
+
]);
|
|
3328
|
+
if (addMarketplace.exitCode !== 0) {
|
|
3329
|
+
const combined = `${addMarketplace.stdout}
|
|
3330
|
+
${addMarketplace.stderr}`.toLowerCase();
|
|
3331
|
+
const isAlreadyRegistered = combined.includes("already");
|
|
3332
|
+
if (!isAlreadyRegistered) {
|
|
3333
|
+
logger.error(
|
|
3334
|
+
`Error: 'claude plugin marketplace add ${BISQUE_MARKETPLACE_SOURCE}' failed (exit ${addMarketplace.exitCode}).
|
|
3335
|
+
stderr: ${addMarketplace.stderr.trim()}`
|
|
3336
|
+
);
|
|
3337
|
+
return 1;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
logger.log(`Installing plugin '${BISQUE_PLUGIN_QUALIFIED}'...`);
|
|
3341
|
+
const install = await runner.run("claude", ["plugin", "install", BISQUE_PLUGIN_QUALIFIED]);
|
|
3342
|
+
if (install.exitCode !== 0) {
|
|
3343
|
+
logger.error(
|
|
3344
|
+
`Error: 'claude plugin install ${BISQUE_PLUGIN_QUALIFIED}' failed (exit ${install.exitCode}).
|
|
3345
|
+
stderr: ${install.stderr.trim()}`
|
|
3346
|
+
);
|
|
3347
|
+
return 1;
|
|
3348
|
+
}
|
|
3349
|
+
logger.log(
|
|
3350
|
+
"\nBisque plugin installed successfully.\nRestart Claude Code (or run /reload-plugins inside an active session) to activate it.\nTry /bisque:run-chain or use the bisque skill once active."
|
|
3351
|
+
);
|
|
3352
|
+
return 0;
|
|
3353
|
+
}
|
|
3354
|
+
function registerPluginCommand(program2, deps = {}) {
|
|
3355
|
+
const pluginCmd = program2.command("plugin").description("Manage the Bisque Claude Code plugin");
|
|
3356
|
+
pluginCmd.command("install").description("Register the Bisque plugin with Claude Code").option("-f, --force", "Force reinstall if already installed", false).action(async (options) => {
|
|
3357
|
+
const exitCode = await runPluginInstall({ force: options.force }, deps);
|
|
3358
|
+
if (exitCode !== 0) {
|
|
3359
|
+
process.exitCode = exitCode;
|
|
3360
|
+
}
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
// src/commands/recall.ts
|
|
3365
|
+
init_api_client();
|
|
3366
|
+
function parseLimit(raw) {
|
|
3367
|
+
const limit = Number.parseInt(raw, 10);
|
|
3368
|
+
if (!Number.isFinite(limit) || limit < 1 || limit > 100) {
|
|
3369
|
+
console.error("Error: --limit must be a positive integer between 1 and 100");
|
|
3370
|
+
return void 0;
|
|
3371
|
+
}
|
|
3372
|
+
return limit;
|
|
3373
|
+
}
|
|
3374
|
+
function parseBudget(raw) {
|
|
3375
|
+
if (raw === void 0) return void 0;
|
|
3376
|
+
const budget = Number.parseInt(raw, 10);
|
|
3377
|
+
if (!Number.isFinite(budget) || budget < 500 || budget > 32e3) {
|
|
3378
|
+
console.error("Error: --budget must be an integer between 500 and 32000");
|
|
3379
|
+
return "error";
|
|
3380
|
+
}
|
|
3381
|
+
return budget;
|
|
3382
|
+
}
|
|
3383
|
+
function renderPackedSummary(result, budget) {
|
|
3384
|
+
if (!result.packed) return;
|
|
3385
|
+
const used = result.used_chars ?? 0;
|
|
3386
|
+
const dropped = result.dropped_count ?? 0;
|
|
3387
|
+
const budgetLabel = budget !== void 0 ? `/${budget}` : "";
|
|
3388
|
+
console.log(`packed ${used}${budgetLabel} chars, ${dropped} dropped`);
|
|
3389
|
+
}
|
|
3390
|
+
function renderItems(result) {
|
|
3391
|
+
if (result.items.length === 0) {
|
|
3392
|
+
console.log("No relevant documents found.");
|
|
3393
|
+
if (result.recall_id !== void 0) {
|
|
3394
|
+
console.log(`recall_id: ${result.recall_id}`);
|
|
3395
|
+
}
|
|
3396
|
+
return;
|
|
3397
|
+
}
|
|
3398
|
+
console.log(`Found ${result.total} relevant document(s):
|
|
3399
|
+
`);
|
|
3400
|
+
for (const item of result.items) {
|
|
3401
|
+
console.log(` ${item.documentId} ${item.title}`);
|
|
3402
|
+
if (item.excerpt) {
|
|
3403
|
+
console.log(` ${item.excerpt.slice(0, 100)}${item.excerpt.length > 100 ? "..." : ""}`);
|
|
3404
|
+
}
|
|
3405
|
+
console.log();
|
|
3406
|
+
}
|
|
3407
|
+
if (result.recall_id !== void 0) {
|
|
3408
|
+
console.log(`recall_id: ${result.recall_id}`);
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
function registerRecallCommand(program2) {
|
|
3412
|
+
program2.command("recall <context>").description("Retrieve documents relevant to a topic or question").option("-l, --limit <number>", "Max results (1-100)", "20").option(
|
|
3413
|
+
"-b, --budget <chars>",
|
|
3414
|
+
"Optional char budget; server packs the highest-scoring subset that fits (500-32000)"
|
|
3415
|
+
).action(async (context, options) => {
|
|
3416
|
+
const limit = parseLimit(options.limit);
|
|
3417
|
+
if (limit === void 0) {
|
|
3418
|
+
process.exitCode = 1;
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
const budget = parseBudget(options.budget);
|
|
3422
|
+
if (budget === "error") {
|
|
3423
|
+
process.exitCode = 1;
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
try {
|
|
3427
|
+
const client = new ApiClient();
|
|
3428
|
+
const requestBody = { context, limit };
|
|
3429
|
+
if (budget !== void 0) requestBody["budget_chars"] = budget;
|
|
3430
|
+
const result = await client.post("/v1/recall", requestBody);
|
|
3431
|
+
recordMeasurement(
|
|
3432
|
+
"recall",
|
|
3433
|
+
budget !== void 0 ? { context, limit, budget_chars: budget } : { context, limit },
|
|
3434
|
+
JSON.stringify(result).length
|
|
3435
|
+
);
|
|
3436
|
+
renderPackedSummary(result, budget);
|
|
3437
|
+
renderItems(result);
|
|
3438
|
+
} catch (error) {
|
|
3439
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3440
|
+
console.error(`Error: ${message}`);
|
|
3441
|
+
process.exitCode = 1;
|
|
3442
|
+
}
|
|
3443
|
+
});
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
// src/commands/recall-annotate.ts
|
|
3447
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, statSync } from "node:fs";
|
|
3448
|
+
import { homedir as homedir3 } from "node:os";
|
|
3449
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
3450
|
+
var ANNOTATION_FILE = join4(homedir3(), ".bisque", "dogfood-incidents-2026-05-25.jsonl");
|
|
3451
|
+
function isPlausibleUuid(id) {
|
|
3452
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
|
3453
|
+
}
|
|
3454
|
+
var MIN_KEY_BYTES = 32;
|
|
3455
|
+
var MIN_HEX_CHARS = MIN_KEY_BYTES * 2;
|
|
3456
|
+
function getHmacKey() {
|
|
3457
|
+
const k = process.env["BISQUE_DOGFOOD_HMAC_KEY"];
|
|
3458
|
+
if (k === void 0 || k.length === 0) return null;
|
|
3459
|
+
const isHex = /^[0-9a-fA-F]+$/.test(k);
|
|
3460
|
+
if (isHex && k.length >= MIN_HEX_CHARS) return k;
|
|
3461
|
+
if (!isHex && Buffer.byteLength(k, "utf8") >= MIN_KEY_BYTES) return k;
|
|
3462
|
+
return null;
|
|
3463
|
+
}
|
|
3464
|
+
var FILE_MODE = 384;
|
|
3465
|
+
var DIR_MODE = 448;
|
|
3466
|
+
function assertSafeFileMode(path2) {
|
|
3467
|
+
try {
|
|
3468
|
+
const mode = statSync(path2).mode & 511;
|
|
3469
|
+
if (mode & 63) {
|
|
3470
|
+
throw new Error(
|
|
3471
|
+
`Annotation file at ${path2} has mode 0${mode.toString(8)} which is wider than 0600 \u2014 refusing to write. chmod 600 to fix.`
|
|
3472
|
+
);
|
|
3473
|
+
}
|
|
3474
|
+
} catch (err) {
|
|
3475
|
+
const code = err.code;
|
|
3476
|
+
if (code === "ENOENT") return;
|
|
3477
|
+
throw err;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
function resolveJudgment(options) {
|
|
3481
|
+
const flags = [];
|
|
3482
|
+
if (options.incident) flags.push("incident");
|
|
3483
|
+
if (options.nonIncident) flags.push("non-incident");
|
|
3484
|
+
if (options.unclear) flags.push("unclear");
|
|
3485
|
+
if (flags.length !== 1) return null;
|
|
3486
|
+
return flags[0];
|
|
3487
|
+
}
|
|
3488
|
+
function registerRecallAnnotateCommand(program2) {
|
|
3489
|
+
program2.command("recall-annotate <recallId>").description(
|
|
3490
|
+
"Annotate a recall response for the T6 dogfood stale-recall log. Local-only \u2014 appends to ~/.bisque/dogfood-incidents-2026-05-25.jsonl. Exactly one of --incident / --non-incident / --unclear is required."
|
|
3491
|
+
).option(
|
|
3492
|
+
"--incident",
|
|
3493
|
+
"Founder judges this recall a stale-promotion incident (counts toward the gate)"
|
|
3494
|
+
).option(
|
|
3495
|
+
"--non-incident",
|
|
3496
|
+
"Founder eyeballed the recall and judged it correctly-ranked (denominator credit; does not count as an incident)"
|
|
3497
|
+
).option(
|
|
3498
|
+
"--unclear",
|
|
3499
|
+
"Founder cannot judge \u2014 the proxy (lastAccessedAt) may be diverging from decision recency. Excluded from incident count; preserved for reviewer audit."
|
|
3500
|
+
).option("--note <text>", "Optional free-text founder context").action(
|
|
3501
|
+
async (recallId, options) => {
|
|
3502
|
+
if (!isPlausibleUuid(recallId)) {
|
|
3503
|
+
console.error(
|
|
3504
|
+
`Error: '${recallId}' does not look like a recall_id (expected UUID 8-4-4-4-12 hex shape).`
|
|
3505
|
+
);
|
|
3506
|
+
process.exitCode = 1;
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
const judgment = resolveJudgment(options);
|
|
3510
|
+
if (judgment === null) {
|
|
3511
|
+
console.error(
|
|
3512
|
+
"Error: exactly one of --incident, --non-incident, or --unclear is required."
|
|
3513
|
+
);
|
|
3514
|
+
process.exitCode = 1;
|
|
3515
|
+
return;
|
|
3516
|
+
}
|
|
3517
|
+
if (getHmacKey() === null) {
|
|
3518
|
+
console.error(
|
|
3519
|
+
"Error: annotation requires BISQUE_DOGFOOD_HMAC_KEY in env (\u226532 bytes utf8 OR \u226564 hex chars, per NIST SP 800-107 \xA75.3.4 for HMAC-SHA256). This is the same HMAC key the backend Lambda uses for queryHash/filepathHash. Source it from your shell profile (or aws ssm get-parameter --name /bisque/dogfood-hmac-key --with-decryption). Without it, this annotation cannot be cross-correlated with CloudWatch EMF emissions. Bootstrap: openssl rand -hex 32"
|
|
3520
|
+
);
|
|
3521
|
+
process.exitCode = 1;
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
const line = {
|
|
3525
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3526
|
+
recallId,
|
|
3527
|
+
founderJudgment: judgment
|
|
3528
|
+
};
|
|
3529
|
+
if (options.note !== void 0 && options.note.length > 0) {
|
|
3530
|
+
line.note = options.note;
|
|
3531
|
+
}
|
|
3532
|
+
try {
|
|
3533
|
+
mkdirSync2(dirname2(ANNOTATION_FILE), { recursive: true, mode: DIR_MODE });
|
|
3534
|
+
assertSafeFileMode(ANNOTATION_FILE);
|
|
3535
|
+
appendFileSync2(ANNOTATION_FILE, `${JSON.stringify(line)}
|
|
3536
|
+
`, {
|
|
3537
|
+
flag: "a",
|
|
3538
|
+
mode: FILE_MODE
|
|
3539
|
+
});
|
|
3540
|
+
console.log(
|
|
3541
|
+
`Annotated recall ${recallId} as ${judgment}. Appended to ${ANNOTATION_FILE}.`
|
|
3542
|
+
);
|
|
3543
|
+
} catch (error) {
|
|
3544
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3545
|
+
console.error(`Error: failed to write annotation: ${message}`);
|
|
3546
|
+
process.exitCode = 1;
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
// src/commands/search.ts
|
|
3553
|
+
init_api_client();
|
|
3554
|
+
function buildSearchParams(query, options) {
|
|
3555
|
+
const params = new URLSearchParams({ q: query, limit: options.limit });
|
|
3556
|
+
if (options.category) {
|
|
3557
|
+
params.set("category", options.category);
|
|
3558
|
+
}
|
|
3559
|
+
if (options.tags) {
|
|
3560
|
+
params.set("tags", options.tags);
|
|
3561
|
+
}
|
|
3562
|
+
if (options.path) {
|
|
3563
|
+
params.set("pathPrefix", options.path);
|
|
3564
|
+
}
|
|
3565
|
+
if (options.cursor) {
|
|
3566
|
+
params.set("cursor", options.cursor);
|
|
3567
|
+
}
|
|
3568
|
+
return params;
|
|
3569
|
+
}
|
|
3570
|
+
function formatResult(result) {
|
|
3571
|
+
const tags = result.tags.length > 0 ? result.tags.join(", ") : "(none)";
|
|
3572
|
+
return ` ${result.title}
|
|
3573
|
+
Path: ${result.filepath}
|
|
3574
|
+
Tags: ${tags}`;
|
|
3575
|
+
}
|
|
3576
|
+
async function executeSearch(query, options) {
|
|
3577
|
+
const client = new ApiClient();
|
|
3578
|
+
const params = buildSearchParams(query, options);
|
|
3579
|
+
const data = await client.get(`/v1/search?${params.toString()}`);
|
|
3580
|
+
if (data.items.length === 0) {
|
|
3581
|
+
console.log("No results found.");
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
for (const result of data.items) {
|
|
3585
|
+
console.log(formatResult(result));
|
|
3586
|
+
console.log();
|
|
3587
|
+
}
|
|
3588
|
+
console.log(`Total: ${data.total} result${data.total === 1 ? "" : "s"}`);
|
|
3589
|
+
if (data.cursor) {
|
|
3590
|
+
console.log(`More results available. Use --cursor ${data.cursor}`);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
function registerSearchCommand(program2) {
|
|
3594
|
+
program2.command("search <query>").description("Search documents").option("-c, --category <category>", "Filter by PARA category").option("-t, --tags <tags>", "Comma-separated tags to filter by").option("--path <prefix>", "Scope search to a path prefix").option("-l, --limit <number>", "Maximum results to return", "20").option("--cursor <value>", "Pagination cursor from previous search").action(async (query, options) => {
|
|
3595
|
+
try {
|
|
3596
|
+
await executeSearch(query, options);
|
|
3597
|
+
} catch (error) {
|
|
3598
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3599
|
+
console.error(`Error: ${message}`);
|
|
3600
|
+
process.exitCode = 1;
|
|
3601
|
+
}
|
|
3602
|
+
});
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
// src/commands/skill.ts
|
|
3606
|
+
import { cp, mkdir as mkdir2, readdir as readdir2, rm, stat as stat2 } from "node:fs/promises";
|
|
3607
|
+
import { homedir as homedir4 } from "node:os";
|
|
3608
|
+
import path from "node:path";
|
|
3609
|
+
var DEFAULT_OPENCLAW_SKILLS_DIR = path.join(homedir4(), ".openclaw", "workspace", "skills");
|
|
3610
|
+
function defaultSourceForTarget(_target) {
|
|
3611
|
+
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
3612
|
+
return path.resolve(here, "..", "..", "..", "plugin-openclaw");
|
|
3613
|
+
}
|
|
3614
|
+
async function exists(p) {
|
|
3615
|
+
try {
|
|
3616
|
+
await stat2(p);
|
|
3617
|
+
return true;
|
|
3618
|
+
} catch {
|
|
3619
|
+
return false;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
async function runSkillInstall(options, deps = {}) {
|
|
3623
|
+
const logger = deps.logger ?? console;
|
|
3624
|
+
if (options.target !== "openclaw") {
|
|
3625
|
+
logger.error(`Unknown target '${options.target}'. Supported: openclaw.`);
|
|
3626
|
+
return 1;
|
|
3627
|
+
}
|
|
3628
|
+
const source = options.source ?? defaultSourceForTarget(options.target);
|
|
3629
|
+
const sourceSkills = path.join(source, "skills");
|
|
3630
|
+
if (!await exists(sourceSkills)) {
|
|
3631
|
+
logger.error(
|
|
3632
|
+
`Source plugin tree not found at ${sourceSkills}. Run 'pnpm --filter @bisque/plugin-openclaw build' first, or pass --source <path>.`
|
|
3633
|
+
);
|
|
3634
|
+
return 1;
|
|
3635
|
+
}
|
|
3636
|
+
const targetDir = deps.openclawSkillsDir ?? DEFAULT_OPENCLAW_SKILLS_DIR;
|
|
3637
|
+
await mkdir2(targetDir, { recursive: true });
|
|
3638
|
+
const entries = await readdir2(sourceSkills, { withFileTypes: true });
|
|
3639
|
+
const skillDirs = entries.filter((e) => e.isDirectory());
|
|
3640
|
+
if (skillDirs.length === 0) {
|
|
3641
|
+
logger.error(`No skill folders found under ${sourceSkills}.`);
|
|
3642
|
+
return 1;
|
|
3643
|
+
}
|
|
3644
|
+
for (const entry of skillDirs) {
|
|
3645
|
+
const dest = path.join(targetDir, entry.name);
|
|
3646
|
+
if (await exists(dest)) {
|
|
3647
|
+
if (!options.force) {
|
|
3648
|
+
logger.error(`Skill '${entry.name}' already exists at ${dest}. Use --force to reinstall.`);
|
|
3649
|
+
return 1;
|
|
3650
|
+
}
|
|
3651
|
+
await rm(dest, { recursive: true, force: true });
|
|
3652
|
+
}
|
|
3653
|
+
await cp(path.join(sourceSkills, entry.name), dest, { recursive: true });
|
|
3654
|
+
logger.log(`Installed skill: ${entry.name}`);
|
|
3655
|
+
}
|
|
3656
|
+
logger.log(`
|
|
3657
|
+
${skillDirs.length} skill(s) installed to ${targetDir}.`);
|
|
3658
|
+
logger.log("Restart your OpenClaw session to pick up the new skills.");
|
|
3659
|
+
return 0;
|
|
3660
|
+
}
|
|
3661
|
+
function registerSkillCommand(program2, deps = {}) {
|
|
3662
|
+
const cmd = program2.command("skill").description("Manage Bisque skill packs for non-Claude-Code platforms");
|
|
3663
|
+
cmd.command("install").description("Install Bisque skills into a target platform").requiredOption("--target <platform>", "Target platform (openclaw)").option("--source <path>", "Source plugin tree (defaults to bundled plugin-openclaw)").option("-f, --force", "Force reinstall, overwriting existing skill folders", false).action(async (options) => {
|
|
3664
|
+
if (options.target !== "openclaw") {
|
|
3665
|
+
console.error(`Unknown target '${options.target}'. Supported: openclaw.`);
|
|
3666
|
+
process.exitCode = 1;
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
const exitCode = await runSkillInstall(
|
|
3670
|
+
{ target: "openclaw", source: options.source, force: options.force },
|
|
3671
|
+
deps
|
|
3672
|
+
);
|
|
3673
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// src/commands/tag.ts
|
|
3678
|
+
init_api_client();
|
|
3679
|
+
function registerTagCommand(program2) {
|
|
3680
|
+
program2.command("tag <docId>").description("Add tags to a document").requiredOption("--tags <tags>", "Comma-separated list of tags").action(async (docId, options) => {
|
|
3681
|
+
try {
|
|
3682
|
+
const tags = options.tags.split(",").map((t) => t.trim());
|
|
3683
|
+
const client = new ApiClient();
|
|
3684
|
+
await client.put(`/v1/documents/${docId}/categorize`, { tags });
|
|
3685
|
+
console.log(`Tagged ${docId}: ${tags.join(", ")}`);
|
|
3686
|
+
} catch (error) {
|
|
3687
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3688
|
+
console.error(`Error: ${message}`);
|
|
3689
|
+
process.exitCode = 1;
|
|
3690
|
+
}
|
|
3691
|
+
});
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
// src/commands/task.ts
|
|
3695
|
+
init_api_client();
|
|
3696
|
+
import { createInterface as createInterface3 } from "node:readline";
|
|
3697
|
+
import chalk8 from "chalk";
|
|
3698
|
+
import Table4 from "cli-table3";
|
|
3699
|
+
import { Option as Option2 } from "commander";
|
|
3700
|
+
var PICKUP_MODES = ["interactive", "autonomous"];
|
|
3701
|
+
function splitCsv(value) {
|
|
3702
|
+
return value.split(",").map((s) => s.trim());
|
|
3703
|
+
}
|
|
3704
|
+
function confirm3(prompt) {
|
|
3705
|
+
const rl = createInterface3({
|
|
3706
|
+
input: process.stdin,
|
|
3707
|
+
output: process.stdout
|
|
3708
|
+
});
|
|
3709
|
+
return new Promise((resolve) => {
|
|
3710
|
+
rl.question(prompt, (answer) => {
|
|
3711
|
+
rl.close();
|
|
3712
|
+
resolve(answer.toLowerCase() === "y");
|
|
3713
|
+
});
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
function registerTaskCommand(program2) {
|
|
3717
|
+
const task = program2.command("task").description("Manage tasks");
|
|
3718
|
+
task.command("create").description("Create a task").requiredOption("--title <title>", "Task title").option("--content <content>", "Full task context").option("--project-path <path>", "PARA project path").option("--chain-id <id>", "Parent chain UUID").option("--chain-order <n>", "Position in chain").option("--assigned-agent-ids <ids>", "Comma-separated agent IDs").option("--reference-docs <docIds>", "Comma-separated doc UUIDs").option("--dependency-task-ids <ids>", "Comma-separated prerequisite task UUIDs").option("--tags <tags>", "Comma-separated tags").addOption(
|
|
3719
|
+
new Option2(
|
|
3720
|
+
"--pickup-mode <mode>",
|
|
3721
|
+
"Restrict who may claim this task (unrestricted when omitted on task)"
|
|
3722
|
+
).choices(PICKUP_MODES)
|
|
3723
|
+
).action(
|
|
3724
|
+
async (options) => {
|
|
3725
|
+
try {
|
|
3726
|
+
let chainOrderValue;
|
|
3727
|
+
if (options.chainOrder) {
|
|
3728
|
+
chainOrderValue = Number.parseFloat(options.chainOrder);
|
|
3729
|
+
if (!Number.isFinite(chainOrderValue) || chainOrderValue < 0) {
|
|
3730
|
+
console.error("Error: --chain-order must be a non-negative number.");
|
|
3731
|
+
process.exitCode = 1;
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
const client = new ApiClient();
|
|
3736
|
+
const body = Object.assign(
|
|
3737
|
+
{ title: options.title },
|
|
3738
|
+
options.content ? { content: options.content } : {},
|
|
3739
|
+
options.projectPath ? { projectPath: options.projectPath } : {},
|
|
3740
|
+
options.chainId ? { chainId: options.chainId } : {},
|
|
3741
|
+
chainOrderValue !== void 0 ? { chainOrder: chainOrderValue } : {},
|
|
3742
|
+
options.assignedAgentIds ? { assignedAgentIds: splitCsv(options.assignedAgentIds) } : {},
|
|
3743
|
+
options.referenceDocs ? { referenceDocs: splitCsv(options.referenceDocs) } : {},
|
|
3744
|
+
options.dependencyTaskIds ? { dependencyTaskIds: splitCsv(options.dependencyTaskIds) } : {},
|
|
3745
|
+
options.tags ? { tags: splitCsv(options.tags) } : {},
|
|
3746
|
+
options.pickupMode ? { pickupMode: options.pickupMode } : {}
|
|
3747
|
+
);
|
|
3748
|
+
const result = await client.post("/v1/tasks", body);
|
|
3749
|
+
const num = String(result.taskNumber).padStart(6, "0");
|
|
3750
|
+
console.log(`Task #${num} created: ${result.documentId}`);
|
|
3751
|
+
} catch (error) {
|
|
3752
|
+
exitWithError(error);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
);
|
|
3756
|
+
task.command("list").description("List tasks").option("--status <status>", "Filter: queued, in-progress, blocked, done, cancelled").option("--project-path <path>", "Filter by project").option("--chain-id <id>", "Filter by chain").addOption(
|
|
3757
|
+
new Option2("--pickup-mode <mode>", "Filter by pickup mode (no filter when omitted)").choices(
|
|
3758
|
+
PICKUP_MODES
|
|
3759
|
+
)
|
|
3760
|
+
).option("--limit <n>", "Max results", "50").action(
|
|
3761
|
+
async (options) => {
|
|
3762
|
+
try {
|
|
3763
|
+
const client = new ApiClient();
|
|
3764
|
+
const params = new URLSearchParams();
|
|
3765
|
+
if (options.status) params.set("status", options.status);
|
|
3766
|
+
if (options.projectPath) params.set("projectPath", options.projectPath);
|
|
3767
|
+
if (options.chainId) params.set("chainId", options.chainId);
|
|
3768
|
+
if (options.pickupMode) params.set("pickupMode", options.pickupMode);
|
|
3769
|
+
params.set("limit", options.limit);
|
|
3770
|
+
const query = params.toString();
|
|
3771
|
+
const path2 = query ? `/v1/tasks?${query}` : "/v1/tasks";
|
|
3772
|
+
const data = await client.get(path2);
|
|
3773
|
+
if (data.length === 0) {
|
|
3774
|
+
console.log(chalk8.yellow("No tasks found."));
|
|
3775
|
+
return;
|
|
3776
|
+
}
|
|
3777
|
+
const table = new Table4({
|
|
3778
|
+
head: [
|
|
3779
|
+
chalk8.cyan("#"),
|
|
3780
|
+
chalk8.cyan("Title"),
|
|
3781
|
+
chalk8.cyan("Status"),
|
|
3782
|
+
chalk8.cyan("ID"),
|
|
3783
|
+
chalk8.cyan("Chain")
|
|
3784
|
+
],
|
|
3785
|
+
style: {
|
|
3786
|
+
head: [],
|
|
3787
|
+
border: ["gray"]
|
|
3788
|
+
}
|
|
3789
|
+
});
|
|
3790
|
+
for (const item of data) {
|
|
3791
|
+
const title = item.title.length > 28 ? `${item.title.slice(0, 27)}...` : item.title;
|
|
3792
|
+
const num = `#${String(item.taskNumber).padStart(6, "0")}`;
|
|
3793
|
+
table.push([num, title, item.status, item.documentId, item.chainId ?? "-"]);
|
|
3794
|
+
}
|
|
3795
|
+
console.log(table.toString());
|
|
3796
|
+
console.log(chalk8.gray(`
|
|
3797
|
+
Total: ${data.length} task(s)`));
|
|
3798
|
+
} catch (error) {
|
|
3799
|
+
exitWithError(error);
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
);
|
|
3803
|
+
task.command("get <taskId>").description("Get a task by ID").action(async (taskId) => {
|
|
3804
|
+
try {
|
|
3805
|
+
const client = new ApiClient();
|
|
3806
|
+
const task2 = await client.get(`/v1/tasks/${taskId}`);
|
|
3807
|
+
console.log(`Title: ${task2.title}`);
|
|
3808
|
+
console.log(`Task #: ${String(task2.taskNumber).padStart(6, "0")}`);
|
|
3809
|
+
console.log(`Status: ${task2.status}`);
|
|
3810
|
+
console.log(`ID: ${task2.documentId}`);
|
|
3811
|
+
console.log(`Filepath: ${task2.filepath}`);
|
|
3812
|
+
if (task2.projectPath) console.log(`Project Path: ${task2.projectPath}`);
|
|
3813
|
+
if (task2.chainId) console.log(`Chain ID: ${task2.chainId}`);
|
|
3814
|
+
if (task2.chainOrder !== void 0) console.log(`Chain Order: ${task2.chainOrder}`);
|
|
3815
|
+
if (task2.tags && task2.tags.length > 0) console.log(`Tags: ${task2.tags.join(", ")}`);
|
|
3816
|
+
console.log(
|
|
3817
|
+
`Dependency Tasks: ${task2.dependencyTaskIds && task2.dependencyTaskIds.length > 0 ? task2.dependencyTaskIds.join(", ") : "none"}`
|
|
3818
|
+
);
|
|
3819
|
+
console.log(
|
|
3820
|
+
`Assigned Agents: ${task2.assignedAgentIds && task2.assignedAgentIds.length > 0 ? task2.assignedAgentIds.join(", ") : "none"}`
|
|
3821
|
+
);
|
|
3822
|
+
console.log(
|
|
3823
|
+
`Reference Docs: ${task2.referenceDocs && task2.referenceDocs.length > 0 ? task2.referenceDocs.join(", ") : "none"}`
|
|
3824
|
+
);
|
|
3825
|
+
console.log(`Created: ${task2.createdAt}`);
|
|
3826
|
+
console.log(`Updated: ${task2.updatedAt}`);
|
|
3827
|
+
if (task2.content) {
|
|
3828
|
+
console.log("---");
|
|
3829
|
+
console.log(task2.content);
|
|
3830
|
+
}
|
|
3831
|
+
} catch (error) {
|
|
3832
|
+
exitWithError(error);
|
|
3833
|
+
}
|
|
3834
|
+
});
|
|
3835
|
+
task.command("update <taskId>").description("Update a task").option("--status <status>", "New status").option("--title <title>", "New title").option("--content <content>", "New content").option("--project-path <path>", "PARA project path").option("--chain-order <n>", "Position in chain (decimals allowed, e.g. 2.5)").option("--assigned-agent-ids <ids>", "Comma-separated agent IDs").option("--reference-docs <docIds>", "Comma-separated doc UUIDs").option("--dependency-task-ids <ids>", "Comma-separated prerequisite task UUIDs").option("--tags <tags>", "Comma-separated tags").option("--completed-at <iso>", "ISO timestamp for completion").addOption(
|
|
3836
|
+
new Option2(
|
|
3837
|
+
"--pickup-mode <mode>",
|
|
3838
|
+
"Restrict who may claim this task (unrestricted when omitted on task)"
|
|
3839
|
+
).choices(PICKUP_MODES)
|
|
3840
|
+
).action(
|
|
3841
|
+
async (taskId, options) => {
|
|
3842
|
+
try {
|
|
3843
|
+
let chainOrderValue;
|
|
3844
|
+
if (options.chainOrder !== void 0) {
|
|
3845
|
+
chainOrderValue = Number.parseFloat(options.chainOrder);
|
|
3846
|
+
if (!Number.isFinite(chainOrderValue) || chainOrderValue < 0) {
|
|
3847
|
+
console.error("Error: --chain-order must be a non-negative number.");
|
|
3848
|
+
process.exitCode = 1;
|
|
3849
|
+
return;
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
const body = Object.assign(
|
|
3853
|
+
{},
|
|
3854
|
+
options.status ? { status: options.status } : {},
|
|
3855
|
+
options.title ? { title: options.title } : {},
|
|
3856
|
+
options.content ? { content: options.content } : {},
|
|
3857
|
+
options.projectPath ? { projectPath: options.projectPath } : {},
|
|
3858
|
+
chainOrderValue !== void 0 ? { chainOrder: chainOrderValue } : {},
|
|
3859
|
+
options.assignedAgentIds ? { assignedAgentIds: splitCsv(options.assignedAgentIds) } : {},
|
|
3860
|
+
options.referenceDocs ? { referenceDocs: splitCsv(options.referenceDocs) } : {},
|
|
3861
|
+
options.dependencyTaskIds ? { dependencyTaskIds: splitCsv(options.dependencyTaskIds) } : {},
|
|
3862
|
+
options.tags ? { tags: splitCsv(options.tags) } : {},
|
|
3863
|
+
options.completedAt ? { completedAt: options.completedAt } : {},
|
|
3864
|
+
options.pickupMode ? { pickupMode: options.pickupMode } : {}
|
|
3865
|
+
);
|
|
3866
|
+
if (Object.keys(body).length === 0) {
|
|
3867
|
+
console.error("Error: Provide at least one field to update.");
|
|
3868
|
+
process.exitCode = 1;
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
const client = new ApiClient();
|
|
3872
|
+
const result = await client.put(`/v1/tasks/${taskId}`, body);
|
|
3873
|
+
console.log(`Task ${result.documentId} updated.`);
|
|
3874
|
+
} catch (error) {
|
|
3875
|
+
exitWithError(error);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
);
|
|
3879
|
+
task.command("claim <taskId>").description("Claim a task for execution").requiredOption("--agent <agentId>", "Agent identifier").addOption(
|
|
3880
|
+
new Option2(
|
|
3881
|
+
"--claimant-class <mode>",
|
|
3882
|
+
"Asserted claimant class (interactive when omitted on claim)"
|
|
3883
|
+
).choices(PICKUP_MODES)
|
|
3884
|
+
).action(
|
|
3885
|
+
async (taskId, options) => {
|
|
3886
|
+
try {
|
|
3887
|
+
const client = new ApiClient();
|
|
3888
|
+
const body = { agentId: options.agent };
|
|
3889
|
+
if (options.claimantClass) {
|
|
3890
|
+
body["claimantClass"] = options.claimantClass;
|
|
3891
|
+
}
|
|
3892
|
+
await client.put(`/v1/tasks/${taskId}/claim`, body);
|
|
3893
|
+
console.log(`Task ${taskId} claimed by ${options.agent}.`);
|
|
3894
|
+
} catch (error) {
|
|
3895
|
+
exitWithError(error);
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
);
|
|
3899
|
+
task.command("delete <taskId>").description("Delete a task").option("-f, --force", "Skip confirmation prompt").action(async (taskId, options) => {
|
|
3900
|
+
try {
|
|
3901
|
+
if (!options.force) {
|
|
3902
|
+
const confirmed = await confirm3(`Delete task ${taskId}? (y/N) `);
|
|
3903
|
+
if (!confirmed) {
|
|
3904
|
+
console.log("Cancelled.");
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
const client = new ApiClient();
|
|
3909
|
+
await client.delete(`/v1/tasks/${taskId}`);
|
|
3910
|
+
console.log(`Task ${taskId} deleted.`);
|
|
3911
|
+
} catch (error) {
|
|
3912
|
+
exitWithError(error);
|
|
3913
|
+
}
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
// src/commands/tree.ts
|
|
3918
|
+
init_api_client();
|
|
3919
|
+
import chalk9 from "chalk";
|
|
3920
|
+
function registerTreeCommand(program2) {
|
|
3921
|
+
program2.command("tree [path]").description(
|
|
3922
|
+
"Recursively fetch documents with content. WARNING: This is the most expensive navigation operation. Prefer 'bisque ls' for navigation and 'bisque batch' for targeted loading."
|
|
3923
|
+
).option("--workspace <id>", "Workspace ID", "default").option("--limit <n>", "Page size (max 25)", "10").option("--max-items <n>", "Total items across all pages (max 200)", "200").option("--json", "Output as JSON").action(
|
|
3924
|
+
async (path2, options) => {
|
|
3925
|
+
try {
|
|
3926
|
+
const client = new ApiClient();
|
|
3927
|
+
const params = new URLSearchParams();
|
|
3928
|
+
if (path2) params.set("path", path2);
|
|
3929
|
+
params.set("workspace", options.workspace);
|
|
3930
|
+
params.set("limit", options.limit);
|
|
3931
|
+
params.set("maxItems", options.maxItems);
|
|
3932
|
+
const result = await client.get(`/v1/documents/tree?${params.toString()}`);
|
|
3933
|
+
if (options.json) {
|
|
3934
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
const data = result;
|
|
3938
|
+
if (!data.documents || data.documents.length === 0) {
|
|
3939
|
+
console.log(chalk9.dim(" (empty)"));
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
for (const doc of data.documents) {
|
|
3943
|
+
console.log(chalk9.blue(`--- ${doc.filepath} ---`));
|
|
3944
|
+
console.log(chalk9.dim(`Title: ${doc.title}`));
|
|
3945
|
+
console.log(doc.content);
|
|
3946
|
+
console.log();
|
|
3947
|
+
}
|
|
3948
|
+
console.log(chalk9.dim(`Returned ${data.totalReturned}/${data.maxItems} max items`));
|
|
3949
|
+
if (data.cursor) {
|
|
3950
|
+
console.log(
|
|
3951
|
+
chalk9.yellow("More results available. Use --json to get cursor for pagination.")
|
|
3952
|
+
);
|
|
3953
|
+
}
|
|
3954
|
+
} catch (err) {
|
|
3955
|
+
exitWithError(err);
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
);
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
// src/commands/update.ts
|
|
3962
|
+
init_api_client();
|
|
3963
|
+
function parseMetadataJson2(raw) {
|
|
3964
|
+
try {
|
|
3965
|
+
const parsed = JSON.parse(raw);
|
|
3966
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
3967
|
+
return { error: "Error: --metadata must be a JSON object" };
|
|
3968
|
+
}
|
|
3969
|
+
return { value: parsed };
|
|
3970
|
+
} catch {
|
|
3971
|
+
return { error: "Error: --metadata must be valid JSON" };
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
function buildUpdateBody(options) {
|
|
3975
|
+
const body = {};
|
|
3976
|
+
if (options.title !== void 0) {
|
|
3977
|
+
body["title"] = options.title;
|
|
3978
|
+
}
|
|
3979
|
+
if (options.content !== void 0) {
|
|
3980
|
+
body["content"] = options.content;
|
|
3981
|
+
}
|
|
3982
|
+
if (options.summary !== void 0) {
|
|
3983
|
+
body["summary"] = options.summary;
|
|
3984
|
+
}
|
|
3985
|
+
if (options.tags !== void 0) {
|
|
3986
|
+
body["tags"] = options.tags.split(",").map((t) => t.trim());
|
|
3987
|
+
}
|
|
3988
|
+
if (options.keywords !== void 0) {
|
|
3989
|
+
body["keywords"] = options.keywords.split(",").map((k) => k.trim());
|
|
3990
|
+
}
|
|
3991
|
+
if (options.metadata !== void 0) {
|
|
3992
|
+
const result = parseMetadataJson2(options.metadata);
|
|
3993
|
+
if ("error" in result) {
|
|
3994
|
+
return result;
|
|
3995
|
+
}
|
|
3996
|
+
body["metadata"] = result.value;
|
|
3997
|
+
}
|
|
3998
|
+
if (options.status !== void 0) {
|
|
3999
|
+
body["status"] = options.status;
|
|
4000
|
+
}
|
|
4001
|
+
if (Object.keys(body).length === 0) {
|
|
4002
|
+
return {
|
|
4003
|
+
error: "Error: at least one update flag is required (--title, --content, --summary, --tags, --keywords, --metadata, --status)"
|
|
4004
|
+
};
|
|
4005
|
+
}
|
|
4006
|
+
return { body };
|
|
4007
|
+
}
|
|
4008
|
+
function registerUpdateCommand(program2) {
|
|
4009
|
+
program2.command("update <docId>").description("Update an existing document").option("--title <title>", "New title").option("--content <content>", "New content").option(
|
|
4010
|
+
"--summary <summary>",
|
|
4011
|
+
"Set the doc's summary field (short abstract, rendered in AGENTS.md ## What's here)"
|
|
4012
|
+
).option("--tags <tags>", "Replace tags (comma-separated)").option("--keywords <keywords>", "Replace keywords (comma-separated)").option("--metadata <json>", "JSON metadata object").option(
|
|
4013
|
+
"--status <status>",
|
|
4014
|
+
"New lifecycle state: live | superseded | archived. Used for manual transitions (e.g. archiving a draft). Supersession-specific transitions go through the v1.5 supersession route."
|
|
4015
|
+
).action(async (docId, options) => {
|
|
4016
|
+
try {
|
|
4017
|
+
const result = buildUpdateBody(options);
|
|
4018
|
+
if ("error" in result) {
|
|
4019
|
+
console.error(result.error);
|
|
4020
|
+
process.exitCode = 1;
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
const client = new ApiClient();
|
|
4024
|
+
await client.put(`/v1/documents/${docId}`, result.body);
|
|
4025
|
+
console.log(`Document ${docId} updated.`);
|
|
4026
|
+
} catch (error) {
|
|
4027
|
+
exitWithError(error);
|
|
4028
|
+
}
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
// src/commands/workspace.ts
|
|
4033
|
+
init_src();
|
|
4034
|
+
init_config();
|
|
4035
|
+
init_api_client();
|
|
4036
|
+
import { createInterface as createInterface4 } from "node:readline/promises";
|
|
4037
|
+
import chalk10 from "chalk";
|
|
4038
|
+
import Table5 from "cli-table3";
|
|
4039
|
+
init_runtime_context();
|
|
4040
|
+
init_workspace_context();
|
|
4041
|
+
async function resolveSubcommandWorkspaceId(options, config) {
|
|
4042
|
+
if (options.workspace) return options.workspace;
|
|
4043
|
+
const runtimeId = getCurrentWorkspaceId();
|
|
4044
|
+
if (runtimeId) return runtimeId;
|
|
4045
|
+
const configActive = await config.get("activeWorkspaceId");
|
|
4046
|
+
if (configActive) return configActive;
|
|
4047
|
+
return void 0;
|
|
4048
|
+
}
|
|
4049
|
+
async function confirm4(prompt, assumeYes) {
|
|
4050
|
+
if (assumeYes) return true;
|
|
4051
|
+
const rl = createInterface4({ input: process.stdin, output: process.stderr });
|
|
4052
|
+
try {
|
|
4053
|
+
const answer = (await rl.question(`${prompt} `)).trim().toLowerCase();
|
|
4054
|
+
return answer === "y" || answer === "yes";
|
|
4055
|
+
} finally {
|
|
4056
|
+
rl.close();
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
function parseExpiresAt(input) {
|
|
4060
|
+
const match = /^(\d+)([smhd]?)$/.exec(input.trim());
|
|
4061
|
+
if (!match) return void 0;
|
|
4062
|
+
const value = Number(match[1]);
|
|
4063
|
+
const unit = match[2] ?? "";
|
|
4064
|
+
const multipliers = { s: 1, "": 1, m: 60, h: 3600, d: 86400 };
|
|
4065
|
+
const seconds = value * (multipliers[unit] ?? 1);
|
|
4066
|
+
return Math.floor(Date.now() / 1e3) + seconds;
|
|
4067
|
+
}
|
|
4068
|
+
function unwrapApiErrorMessage(err) {
|
|
4069
|
+
if (err instanceof Error) return err.message;
|
|
4070
|
+
return String(err);
|
|
4071
|
+
}
|
|
4072
|
+
async function workspaceList(json) {
|
|
4073
|
+
try {
|
|
4074
|
+
const config = new Config();
|
|
4075
|
+
const cache = await refreshWorkspacesCache(config);
|
|
4076
|
+
if (json) {
|
|
4077
|
+
console.log(JSON.stringify(cache, null, 2));
|
|
4078
|
+
return;
|
|
4079
|
+
}
|
|
4080
|
+
if (cache.workspaces.length === 0) {
|
|
4081
|
+
console.log(chalk10.yellow("No workspaces found."));
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
4084
|
+
const personal = cache.workspaces.filter((w) => w.isPersonal);
|
|
4085
|
+
const shared = cache.workspaces.filter((w) => !w.isPersonal);
|
|
4086
|
+
const renderSection2 = (label, items) => {
|
|
4087
|
+
if (items.length === 0) return;
|
|
4088
|
+
console.log(chalk10.bold.cyan(`
|
|
4089
|
+
${label}`));
|
|
4090
|
+
const table = new Table5({
|
|
4091
|
+
head: [
|
|
4092
|
+
chalk10.cyan("ID"),
|
|
4093
|
+
chalk10.cyan("Name"),
|
|
4094
|
+
chalk10.cyan("Role"),
|
|
4095
|
+
chalk10.cyan("Last accessed")
|
|
4096
|
+
],
|
|
4097
|
+
style: { head: [], border: ["gray"] }
|
|
4098
|
+
});
|
|
4099
|
+
for (const ws of items) {
|
|
4100
|
+
const last = ws.lastAccessedAt ? new Date(ws.lastAccessedAt).toLocaleString() : "-";
|
|
4101
|
+
table.push([ws.workspaceId, ws.name, ws.role, last]);
|
|
4102
|
+
}
|
|
4103
|
+
console.log(table.toString());
|
|
4104
|
+
};
|
|
4105
|
+
renderSection2("Personal", personal);
|
|
4106
|
+
renderSection2("Shared", shared);
|
|
4107
|
+
console.log(chalk10.gray(`
|
|
4108
|
+
Total: ${cache.workspaces.length} workspace(s)`));
|
|
4109
|
+
} catch (error) {
|
|
4110
|
+
exitWithError(error);
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
async function workspaceSwitch(id) {
|
|
4114
|
+
try {
|
|
4115
|
+
const config = new Config();
|
|
4116
|
+
const cache = await refreshWorkspacesCache(config);
|
|
4117
|
+
const match = cache.workspaces.find((w) => w.workspaceId === id);
|
|
4118
|
+
if (!match) {
|
|
4119
|
+
console.error(chalk10.red(`Error: workspace '${id}' not found among your memberships.`));
|
|
4120
|
+
console.error(
|
|
4121
|
+
chalk10.gray(
|
|
4122
|
+
"Run `bisque workspace list` to see what you can access. To join a workspace, run `bisque workspace join <code>`."
|
|
4123
|
+
)
|
|
4124
|
+
);
|
|
4125
|
+
process.exit(3);
|
|
4126
|
+
}
|
|
4127
|
+
await config.set("activeWorkspaceId", id);
|
|
4128
|
+
console.log(chalk10.green(`Active workspace set: ${match.name} (${match.role})`));
|
|
4129
|
+
console.log(
|
|
4130
|
+
chalk10.yellow(
|
|
4131
|
+
"Note: Agents must still pass `--workspace <id>` explicitly on every mutating operation. This setting is a human convenience and is NOT read by agents."
|
|
4132
|
+
)
|
|
4133
|
+
);
|
|
4134
|
+
} catch (error) {
|
|
4135
|
+
exitWithError(error);
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
async function workspaceCreate(name, options) {
|
|
4139
|
+
try {
|
|
4140
|
+
const config = new Config();
|
|
4141
|
+
const client = new ApiClient(config);
|
|
4142
|
+
const ws = await client.post("/v1/workspaces", { name });
|
|
4143
|
+
if (options.json) {
|
|
4144
|
+
console.log(JSON.stringify(ws, null, 2));
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
4147
|
+
console.log(chalk10.green(`Workspace created: ${ws.name}`));
|
|
4148
|
+
console.log(` ID: ${ws.workspaceId}`);
|
|
4149
|
+
console.log(` Owner: ${ws.ownerUserId}`);
|
|
4150
|
+
const yes = options.yes === true || await confirm4("Switch to this workspace now? [y/N]", false);
|
|
4151
|
+
if (yes) {
|
|
4152
|
+
await refreshWorkspacesCache(config);
|
|
4153
|
+
await config.set("activeWorkspaceId", ws.workspaceId);
|
|
4154
|
+
console.log(chalk10.green(`Active workspace set: ${ws.name} (owner)`));
|
|
4155
|
+
} else {
|
|
4156
|
+
console.log(
|
|
4157
|
+
chalk10.gray(
|
|
4158
|
+
`Workspace ready. Run \`bisque workspace switch ${ws.workspaceId}\` to make it active.`
|
|
4159
|
+
)
|
|
4160
|
+
);
|
|
4161
|
+
}
|
|
4162
|
+
} catch (error) {
|
|
4163
|
+
exitWithError(error);
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
async function workspaceInvite(options) {
|
|
4167
|
+
try {
|
|
4168
|
+
const config = new Config();
|
|
4169
|
+
const targetWs = await resolveSubcommandWorkspaceId(options, config);
|
|
4170
|
+
if (!targetWs) {
|
|
4171
|
+
console.error(
|
|
4172
|
+
chalk10.red(
|
|
4173
|
+
"Error: no active workspace. Use --workspace <id> or run `bisque workspace switch`."
|
|
4174
|
+
)
|
|
4175
|
+
);
|
|
4176
|
+
process.exit(5);
|
|
4177
|
+
}
|
|
4178
|
+
const maxRedemptions = options.maxUses ? Number(options.maxUses) : 1;
|
|
4179
|
+
if (!Number.isInteger(maxRedemptions) || maxRedemptions < 1 || maxRedemptions > 100) {
|
|
4180
|
+
console.error(chalk10.red("Error: --max-uses must be an integer between 1 and 100."));
|
|
4181
|
+
process.exit(1);
|
|
4182
|
+
}
|
|
4183
|
+
const expiresIn = options.expires ?? "7d";
|
|
4184
|
+
const expiresAt = parseExpiresAt(expiresIn);
|
|
4185
|
+
if (expiresAt === void 0) {
|
|
4186
|
+
console.error(
|
|
4187
|
+
chalk10.red("Error: --expires must be a duration like '7d', '24h', '30m' (max 30d).")
|
|
4188
|
+
);
|
|
4189
|
+
process.exit(1);
|
|
4190
|
+
}
|
|
4191
|
+
const maxExpiresAt = Math.floor(Date.now() / 1e3) + 30 * 86400;
|
|
4192
|
+
if (expiresAt > maxExpiresAt) {
|
|
4193
|
+
console.error(chalk10.red("Error: --expires must not exceed 30 days."));
|
|
4194
|
+
process.exit(1);
|
|
4195
|
+
}
|
|
4196
|
+
const body = {
|
|
4197
|
+
role: options.role ?? "member",
|
|
4198
|
+
maxRedemptions,
|
|
4199
|
+
expiresAt
|
|
4200
|
+
};
|
|
4201
|
+
const client = new ApiClient(config, { workspaceId: targetWs });
|
|
4202
|
+
const created = await client.post(
|
|
4203
|
+
`/v1/workspaces/${targetWs}/invite-codes`,
|
|
4204
|
+
body
|
|
4205
|
+
);
|
|
4206
|
+
if (options.json) {
|
|
4207
|
+
console.log(JSON.stringify(created, null, 2));
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
const expiresHuman = new Date(created.expiresAt * 1e3).toLocaleString();
|
|
4211
|
+
console.log(chalk10.green(`Invite code created: ${chalk10.bold(created.code)}`));
|
|
4212
|
+
console.log(` Role: ${created.roleOnRedeem}`);
|
|
4213
|
+
console.log(` Max uses: ${created.maxRedemptions}`);
|
|
4214
|
+
console.log(` Expires: ${expiresHuman}`);
|
|
4215
|
+
console.log("");
|
|
4216
|
+
console.log(chalk10.gray("Share with your teammate:"));
|
|
4217
|
+
console.log(chalk10.bold(` bisque workspace join ${created.code}`));
|
|
4218
|
+
} catch (error) {
|
|
4219
|
+
exitWithError(error);
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
async function workspaceJoin(code, options) {
|
|
4223
|
+
try {
|
|
4224
|
+
const config = new Config();
|
|
4225
|
+
const client = new ApiClient(config);
|
|
4226
|
+
const result = await client.post("/v1/workspaces/join", { code });
|
|
4227
|
+
await refreshWorkspacesCache(config);
|
|
4228
|
+
if (options.json) {
|
|
4229
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4230
|
+
return;
|
|
4231
|
+
}
|
|
4232
|
+
const verb = result.idempotent ? "Already a member of" : "Joined";
|
|
4233
|
+
console.log(chalk10.green(`${verb} workspace: ${result.workspaceId} (role: ${result.role})`));
|
|
4234
|
+
const yes = options.yes === true || await confirm4("Switch to this workspace now? [y/N]", false);
|
|
4235
|
+
if (yes) {
|
|
4236
|
+
await config.set("activeWorkspaceId", result.workspaceId);
|
|
4237
|
+
console.log(chalk10.green("Active workspace updated."));
|
|
4238
|
+
}
|
|
4239
|
+
} catch (error) {
|
|
4240
|
+
exitWithError(error);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
async function workspaceMembers(options) {
|
|
4244
|
+
try {
|
|
4245
|
+
const config = new Config();
|
|
4246
|
+
const targetWs = await resolveSubcommandWorkspaceId(options, config);
|
|
4247
|
+
if (!targetWs) {
|
|
4248
|
+
console.error(
|
|
4249
|
+
chalk10.red(
|
|
4250
|
+
"Error: no workspace selected. Use --workspace <id> or run `bisque workspace switch`."
|
|
4251
|
+
)
|
|
4252
|
+
);
|
|
4253
|
+
process.exit(5);
|
|
4254
|
+
}
|
|
4255
|
+
const client = new ApiClient(config, { workspaceId: targetWs });
|
|
4256
|
+
const resp = await client.get(`/v1/workspaces/${targetWs}/members`);
|
|
4257
|
+
const members = resp.members ?? [];
|
|
4258
|
+
if (options.json) {
|
|
4259
|
+
console.log(JSON.stringify(members, null, 2));
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
if (members.length === 0) {
|
|
4263
|
+
console.log(chalk10.yellow("No members found."));
|
|
4264
|
+
return;
|
|
4265
|
+
}
|
|
4266
|
+
const table = new Table5({
|
|
4267
|
+
head: [
|
|
4268
|
+
chalk10.cyan("User ID"),
|
|
4269
|
+
chalk10.cyan("Role"),
|
|
4270
|
+
chalk10.cyan("Joined"),
|
|
4271
|
+
chalk10.cyan("Last accessed")
|
|
4272
|
+
],
|
|
4273
|
+
style: { head: [], border: ["gray"] }
|
|
4274
|
+
});
|
|
4275
|
+
for (const m of members) {
|
|
4276
|
+
const joined = new Date(m.joinedAt).toLocaleString();
|
|
4277
|
+
const last = m.lastAccessedAt ? new Date(m.lastAccessedAt).toLocaleString() : "-";
|
|
4278
|
+
table.push([m.userId, m.role, joined, last]);
|
|
4279
|
+
}
|
|
4280
|
+
console.log(table.toString());
|
|
4281
|
+
console.log(chalk10.gray(`
|
|
4282
|
+
Total: ${members.length} member(s)`));
|
|
4283
|
+
} catch (error) {
|
|
4284
|
+
exitWithError(error);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
async function workspaceTransfer(targetUserId, options) {
|
|
4288
|
+
try {
|
|
4289
|
+
const config = new Config();
|
|
4290
|
+
const targetWs = await resolveSubcommandWorkspaceId(options, config);
|
|
4291
|
+
if (!targetWs) {
|
|
4292
|
+
console.error(
|
|
4293
|
+
chalk10.red(
|
|
4294
|
+
"Error: no workspace selected. Use --workspace <id> or run `bisque workspace switch`."
|
|
4295
|
+
)
|
|
4296
|
+
);
|
|
4297
|
+
process.exit(5);
|
|
4298
|
+
}
|
|
4299
|
+
const client = new ApiClient(config, { workspaceId: targetWs });
|
|
4300
|
+
if (options.clearSuccessor) {
|
|
4301
|
+
const result2 = await client.post(`/v1/workspaces/${targetWs}/ownership-transfer`, {
|
|
4302
|
+
mode: "set-successor",
|
|
4303
|
+
successorUserId: null
|
|
4304
|
+
});
|
|
4305
|
+
if (options.json) {
|
|
4306
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4309
|
+
console.log(chalk10.green(`Successor cleared for workspace ${result2.name}.`));
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
const result = await client.post(`/v1/workspaces/${targetWs}/ownership-transfer`, {
|
|
4313
|
+
mode: "transfer",
|
|
4314
|
+
newOwnerUserId: targetUserId
|
|
4315
|
+
});
|
|
4316
|
+
if (options.json) {
|
|
4317
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4318
|
+
return;
|
|
4319
|
+
}
|
|
4320
|
+
console.log(
|
|
4321
|
+
chalk10.green(`Ownership transferred to ${targetUserId} for workspace ${result.name}.`)
|
|
4322
|
+
);
|
|
4323
|
+
} catch (error) {
|
|
4324
|
+
exitWithError(error);
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
async function workspaceSetSuccessor(userIdOrNull, options) {
|
|
4328
|
+
try {
|
|
4329
|
+
const config = new Config();
|
|
4330
|
+
const targetWs = await resolveSubcommandWorkspaceId(options, config);
|
|
4331
|
+
if (!targetWs) {
|
|
4332
|
+
console.error(
|
|
4333
|
+
chalk10.red(
|
|
4334
|
+
"Error: no workspace selected. Use --workspace <id> or run `bisque workspace switch`."
|
|
4335
|
+
)
|
|
4336
|
+
);
|
|
4337
|
+
process.exit(5);
|
|
4338
|
+
}
|
|
4339
|
+
const successorUserId = userIdOrNull.toLowerCase() === "null" ? null : userIdOrNull;
|
|
4340
|
+
const client = new ApiClient(config, { workspaceId: targetWs });
|
|
4341
|
+
const result = await client.post(`/v1/workspaces/${targetWs}/ownership-transfer`, {
|
|
4342
|
+
mode: "set-successor",
|
|
4343
|
+
successorUserId
|
|
4344
|
+
});
|
|
4345
|
+
if (options.json) {
|
|
4346
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4347
|
+
return;
|
|
4348
|
+
}
|
|
4349
|
+
console.log(
|
|
4350
|
+
chalk10.green(
|
|
4351
|
+
successorUserId ? `Successor set to ${successorUserId} for workspace ${result.name}.` : `Successor cleared for workspace ${result.name}.`
|
|
4352
|
+
)
|
|
4353
|
+
);
|
|
4354
|
+
} catch (error) {
|
|
4355
|
+
exitWithError(error);
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
async function workspaceLeave(options) {
|
|
4359
|
+
try {
|
|
4360
|
+
const config = new Config();
|
|
4361
|
+
const targetWs = await resolveSubcommandWorkspaceId(options, config);
|
|
4362
|
+
if (!targetWs) {
|
|
4363
|
+
console.error(
|
|
4364
|
+
chalk10.red(
|
|
4365
|
+
"Error: no workspace selected. Use --workspace <id> or run `bisque workspace switch`."
|
|
4366
|
+
)
|
|
4367
|
+
);
|
|
4368
|
+
process.exit(5);
|
|
4369
|
+
}
|
|
4370
|
+
const selfId = await getSelfUserId(config);
|
|
4371
|
+
if (!selfId) {
|
|
4372
|
+
console.error(chalk10.red("Error: could not resolve your user id -- are you authenticated?"));
|
|
4373
|
+
process.exit(2);
|
|
4374
|
+
}
|
|
4375
|
+
const ok = await confirm4(
|
|
4376
|
+
`Leave workspace ${targetWs}? You will need a new invite code to rejoin. [y/N]`,
|
|
4377
|
+
options.yes === true
|
|
4378
|
+
);
|
|
4379
|
+
if (!ok) {
|
|
4380
|
+
console.log(chalk10.gray("Aborted."));
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
const client = new ApiClient(config, { workspaceId: targetWs });
|
|
4384
|
+
await client.delete(`/v1/workspaces/${targetWs}/members/${selfId}`);
|
|
4385
|
+
const active = await config.get("activeWorkspaceId");
|
|
4386
|
+
if (active === targetWs) {
|
|
4387
|
+
await config.set("activeWorkspaceId", void 0);
|
|
4388
|
+
}
|
|
4389
|
+
if (options.json) {
|
|
4390
|
+
console.log(JSON.stringify({ workspaceId: targetWs, left: true }, null, 2));
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
console.log(chalk10.green(`Left workspace ${targetWs}.`));
|
|
4394
|
+
} catch (error) {
|
|
4395
|
+
exitWithError(error);
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
function renderStructure(structure) {
|
|
4399
|
+
const sections = [
|
|
4400
|
+
{ label: "Projects", folders: structure?.projects ?? [] },
|
|
4401
|
+
{ label: "Areas", folders: structure?.areas ?? [] },
|
|
4402
|
+
{ label: "Resources", folders: structure?.resources ?? [] },
|
|
4403
|
+
{ label: "Archive", folders: structure?.archive ?? [] }
|
|
4404
|
+
];
|
|
4405
|
+
const totalFolders = sections.reduce((n, s) => n + s.folders.length, 0);
|
|
4406
|
+
if (totalFolders === 0) {
|
|
4407
|
+
console.log(chalk10.yellow("Workspace is empty -- no folders found."));
|
|
4408
|
+
return;
|
|
4409
|
+
}
|
|
4410
|
+
for (const { label, folders } of sections) {
|
|
4411
|
+
if (folders.length === 0) continue;
|
|
4412
|
+
console.log(chalk10.bold.cyan(`
|
|
4413
|
+
${label}`));
|
|
4414
|
+
for (const folder of folders) {
|
|
4415
|
+
console.log(` ${folder}`);
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
console.log("");
|
|
4419
|
+
}
|
|
4420
|
+
function renderDeleteResult(result, json) {
|
|
4421
|
+
if (json) {
|
|
4422
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4423
|
+
return;
|
|
4424
|
+
}
|
|
4425
|
+
if (result.status === "deleted") {
|
|
4426
|
+
console.log(chalk10.green("Workspace deleted (cascade complete)."));
|
|
4427
|
+
return;
|
|
4428
|
+
}
|
|
4429
|
+
if (result.status === "pending-async") {
|
|
4430
|
+
console.log(
|
|
4431
|
+
chalk10.green(
|
|
4432
|
+
`Workspace soft-deleted; large-cascade scheduled (job: ${result.jobId}). Membership has been revoked; data drain happens asynchronously.`
|
|
4433
|
+
)
|
|
4434
|
+
);
|
|
4435
|
+
return;
|
|
4436
|
+
}
|
|
4437
|
+
if (result.status === "transferred") {
|
|
4438
|
+
console.log(
|
|
4439
|
+
chalk10.green(
|
|
4440
|
+
`Ownership transferred to ${result.workspace.ownerUserId}. New owner of '${result.workspace.name}'.`
|
|
4441
|
+
)
|
|
4442
|
+
);
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
function renderDeleteConfirmMenu(resp) {
|
|
4447
|
+
console.log(chalk10.bold("\nDelete workspace -- confirmation required"));
|
|
4448
|
+
console.log(` Workspace: ${resp.workspace.name} (${resp.workspace.id})`);
|
|
4449
|
+
console.log(` Members: ${resp.workspace.memberCount}`);
|
|
4450
|
+
console.log(` Admins: ${resp.workspace.adminCount}`);
|
|
4451
|
+
console.log(chalk10.bold("\nAvailable actions:"));
|
|
4452
|
+
for (const opt of resp.options) {
|
|
4453
|
+
if (opt.action === "transfer-to") {
|
|
4454
|
+
console.log(` - transfer-to=<userId> ${opt.label}`);
|
|
4455
|
+
if (opt.validRecipients && opt.validRecipients.length > 0) {
|
|
4456
|
+
console.log(chalk10.gray(` eligible: ${opt.validRecipients.join(", ")}`));
|
|
4457
|
+
}
|
|
4458
|
+
} else {
|
|
4459
|
+
console.log(` - cascade-delete ${opt.label}`);
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
console.log(
|
|
4463
|
+
chalk10.gray("\nRe-run with --confirm cascade-delete OR --confirm transfer-to=<userId>")
|
|
4464
|
+
);
|
|
4465
|
+
}
|
|
4466
|
+
async function buildDeleteQueryString(action, targetWs, assumeYes) {
|
|
4467
|
+
if (action === "cascade-delete") {
|
|
4468
|
+
const ok = await confirm4(
|
|
4469
|
+
`Cascade-delete workspace ${targetWs}? This cannot be undone. [y/N]`,
|
|
4470
|
+
assumeYes
|
|
4471
|
+
);
|
|
4472
|
+
if (!ok) {
|
|
4473
|
+
console.log(chalk10.gray("Aborted."));
|
|
4474
|
+
return void 0;
|
|
4475
|
+
}
|
|
4476
|
+
return "confirm=true&action=cascade-delete";
|
|
4477
|
+
}
|
|
4478
|
+
if (action.startsWith("transfer-to=")) {
|
|
4479
|
+
const recipient = action.slice("transfer-to=".length);
|
|
4480
|
+
if (!recipient) {
|
|
4481
|
+
console.error(chalk10.red("Error: --confirm transfer-to=<userId> requires a userId."));
|
|
4482
|
+
process.exit(1);
|
|
4483
|
+
}
|
|
4484
|
+
return `confirm=true&action=transfer-to&recipient=${encodeURIComponent(recipient)}`;
|
|
4485
|
+
}
|
|
4486
|
+
console.error(
|
|
4487
|
+
chalk10.red(
|
|
4488
|
+
`Error: --confirm must be 'cascade-delete' or 'transfer-to=<userId>' (got: ${action})`
|
|
4489
|
+
)
|
|
4490
|
+
);
|
|
4491
|
+
process.exit(1);
|
|
4492
|
+
}
|
|
4493
|
+
async function runDeletePhase1(client, targetWs, json) {
|
|
4494
|
+
const resp = await client.delete(`/v1/workspaces/${targetWs}`);
|
|
4495
|
+
if (!resp || typeof resp === "object" && !("status" in resp)) {
|
|
4496
|
+
if (json) {
|
|
4497
|
+
console.log(JSON.stringify({ status: "deleted" }, null, 2));
|
|
4498
|
+
return;
|
|
4499
|
+
}
|
|
4500
|
+
console.log(chalk10.green("Workspace deleted."));
|
|
4501
|
+
return;
|
|
4502
|
+
}
|
|
4503
|
+
if (json) {
|
|
4504
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
renderDeleteConfirmMenu(resp);
|
|
4508
|
+
}
|
|
4509
|
+
function classifyDeleteResponse(raw) {
|
|
4510
|
+
if (raw === void 0) {
|
|
4511
|
+
return { status: "deleted" };
|
|
4512
|
+
}
|
|
4513
|
+
if (typeof raw === "object" && raw !== null && "status" in raw && raw.status === "pending-async") {
|
|
4514
|
+
const j = raw;
|
|
4515
|
+
return { status: "pending-async", jobId: j.jobId };
|
|
4516
|
+
}
|
|
4517
|
+
return { status: "transferred", workspace: raw };
|
|
4518
|
+
}
|
|
4519
|
+
async function workspaceDelete(options) {
|
|
4520
|
+
try {
|
|
4521
|
+
const config = new Config();
|
|
4522
|
+
const targetWs = await resolveSubcommandWorkspaceId(options, config);
|
|
4523
|
+
if (!targetWs) {
|
|
4524
|
+
console.error(
|
|
4525
|
+
chalk10.red(
|
|
4526
|
+
"Error: no workspace selected. Use --workspace <id> or run `bisque workspace switch`."
|
|
4527
|
+
)
|
|
4528
|
+
);
|
|
4529
|
+
process.exit(5);
|
|
4530
|
+
}
|
|
4531
|
+
const client = new ApiClient(config, { workspaceId: targetWs });
|
|
4532
|
+
if (!options.confirm) {
|
|
4533
|
+
await runDeletePhase1(client, targetWs, options.json === true);
|
|
4534
|
+
return;
|
|
4535
|
+
}
|
|
4536
|
+
const queryString = await buildDeleteQueryString(
|
|
4537
|
+
options.confirm,
|
|
4538
|
+
targetWs,
|
|
4539
|
+
options.yes === true
|
|
4540
|
+
);
|
|
4541
|
+
if (queryString === void 0) {
|
|
4542
|
+
return;
|
|
4543
|
+
}
|
|
4544
|
+
const raw = await client.delete(`/v1/workspaces/${targetWs}?${queryString}`);
|
|
4545
|
+
const result = classifyDeleteResponse(raw);
|
|
4546
|
+
const active = await config.get("activeWorkspaceId");
|
|
4547
|
+
if (active === targetWs) {
|
|
4548
|
+
await config.set("activeWorkspaceId", void 0);
|
|
4549
|
+
}
|
|
4550
|
+
renderDeleteResult(result, options.json === true);
|
|
4551
|
+
} catch (error) {
|
|
4552
|
+
if (error instanceof BisqueApiError) {
|
|
4553
|
+
exitWithError(error);
|
|
4554
|
+
}
|
|
4555
|
+
const message = unwrapApiErrorMessage(error);
|
|
4556
|
+
console.error(chalk10.red(`Error: ${message}`));
|
|
4557
|
+
process.exit(1);
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
function registerWorkspaceCommand(program2) {
|
|
4561
|
+
const workspace = program2.command("workspace").description("Manage workspaces (shared + personal)");
|
|
4562
|
+
workspace.command("list").description("List your workspaces (personal first, then shared)").option("--json", "Output as JSON").action(async (options) => {
|
|
4563
|
+
await workspaceList(options.json === true);
|
|
4564
|
+
});
|
|
4565
|
+
workspace.command("switch <id>").description("Set the active workspace for human-convenience use").action(async (id) => {
|
|
4566
|
+
await workspaceSwitch(id);
|
|
4567
|
+
});
|
|
4568
|
+
workspace.command("create <name>").description("Create a new shared workspace (you become its owner)").option("--json", "Output as JSON").option("-y, --yes", "Auto-switch to the new workspace without prompting").action(async (name, options) => {
|
|
4569
|
+
await workspaceCreate(name, options);
|
|
4570
|
+
});
|
|
4571
|
+
workspace.command("invite").description("Generate a new invite code (admin/owner only)").option("--role <role>", "Role granted to the invitee (admin|member|viewer)", "member").option("--max-uses <n>", "Maximum number of redemptions (1-100)", "1").option("--expires <duration>", "Expiration window (e.g. 7d, 24h, 30m; max 30d)", "7d").option("--workspace <id>", "Workspace to issue the invite against (defaults to active)").option("--json", "Output as JSON").action(
|
|
4572
|
+
async (options) => {
|
|
4573
|
+
await workspaceInvite(options);
|
|
4574
|
+
}
|
|
4575
|
+
);
|
|
4576
|
+
workspace.command("join <code>").description("Redeem an invite code").option("-y, --yes", "Auto-switch to the joined workspace without prompting").option("--json", "Output as JSON").action(async (code, options) => {
|
|
4577
|
+
await workspaceJoin(code, options);
|
|
4578
|
+
});
|
|
4579
|
+
workspace.command("members").description("List members of a workspace (defaults to active)").option("--workspace <id>", "Workspace to list (defaults to active)").option("--json", "Output as JSON").action(async (options) => {
|
|
4580
|
+
await workspaceMembers(options);
|
|
4581
|
+
});
|
|
4582
|
+
workspace.command("transfer <userId>").description("Transfer ownership of a workspace (owner only)").option("--workspace <id>", "Workspace to transfer (defaults to active)").option("--clear-successor", "Clear the successor instead of transferring ownership now").option("--json", "Output as JSON").action(
|
|
4583
|
+
async (userId, options) => {
|
|
4584
|
+
await workspaceTransfer(userId, options);
|
|
4585
|
+
}
|
|
4586
|
+
);
|
|
4587
|
+
workspace.command("set-successor <userIdOrNull>").description(
|
|
4588
|
+
"Set or clear the workspace's ownership successor (owner only). Pass 'null' to clear."
|
|
4589
|
+
).option("--workspace <id>", "Workspace to update (defaults to active)").option("--json", "Output as JSON").action(async (userIdOrNull, options) => {
|
|
4590
|
+
await workspaceSetSuccessor(userIdOrNull, options);
|
|
4591
|
+
});
|
|
4592
|
+
workspace.command("leave").description("Leave a workspace (you must not be the sole owner)").option("--workspace <id>", "Workspace to leave (defaults to active)").option("-y, --yes", "Skip confirmation prompt").option("--json", "Output as JSON").action(async (options) => {
|
|
4593
|
+
await workspaceLeave(options);
|
|
4594
|
+
});
|
|
4595
|
+
workspace.command("delete").description("Delete a workspace (two-phase: first call shows options, second call executes)").option("--workspace <id>", "Workspace to delete (defaults to active)").option(
|
|
4596
|
+
"--confirm <action>",
|
|
4597
|
+
"Phase 2: 'cascade-delete' or 'transfer-to=<userId>' (omit for the options menu)"
|
|
4598
|
+
).option("-y, --yes", "Skip the interactive y/N prompt on cascade-delete").option("--json", "Output as JSON").action(
|
|
4599
|
+
async (options) => {
|
|
4600
|
+
await workspaceDelete(options);
|
|
4601
|
+
}
|
|
4602
|
+
);
|
|
4603
|
+
workspace.command("structure").description("Show PARA folder hierarchy for the resolved workspace").action(async () => {
|
|
4604
|
+
try {
|
|
4605
|
+
const client = new ApiClient();
|
|
4606
|
+
const result = await client.get("/v1/workspace/structure");
|
|
4607
|
+
renderStructure(result.structure);
|
|
4608
|
+
} catch (error) {
|
|
4609
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4610
|
+
console.error(chalk10.red(`Error: ${message}`));
|
|
4611
|
+
process.exitCode = 1;
|
|
4612
|
+
}
|
|
4613
|
+
});
|
|
4614
|
+
workspace.command("get <id>").description("Get personal-workspace details (legacy)").option("--json", "Output as JSON").action(async (id, options) => {
|
|
4615
|
+
try {
|
|
4616
|
+
const client = new ApiClient();
|
|
4617
|
+
const ws = await client.get(`/v1/workspaces/${id}`);
|
|
4618
|
+
if (options.json) {
|
|
4619
|
+
console.log(JSON.stringify(ws, null, 2));
|
|
4620
|
+
return;
|
|
4621
|
+
}
|
|
4622
|
+
console.log(chalk10.bold("\nWorkspace Details\n"));
|
|
4623
|
+
console.log(`${chalk10.cyan("ID:")} ${ws.workspaceId}`);
|
|
4624
|
+
console.log(`${chalk10.cyan("Name:")} ${ws.name}`);
|
|
4625
|
+
console.log(`${chalk10.cyan("User ID:")} ${ws.userId}`);
|
|
4626
|
+
console.log(`${chalk10.cyan("Created:")} ${new Date(ws.createdAt).toLocaleString()}`);
|
|
4627
|
+
console.log(`${chalk10.cyan("Updated:")} ${new Date(ws.updatedAt).toLocaleString()}`);
|
|
4628
|
+
} catch (error) {
|
|
4629
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4630
|
+
console.error(chalk10.red(`Error: ${message}`));
|
|
4631
|
+
process.exitCode = 1;
|
|
4632
|
+
}
|
|
4633
|
+
});
|
|
4634
|
+
workspace.command("default").description("Show personal-default workspace (legacy)").option("--json", "Output as JSON").action(async (options) => {
|
|
4635
|
+
try {
|
|
4636
|
+
const client = new ApiClient();
|
|
4637
|
+
const ws = await client.get("/v1/workspaces/default");
|
|
4638
|
+
if (options.json) {
|
|
4639
|
+
console.log(JSON.stringify(ws, null, 2));
|
|
4640
|
+
return;
|
|
4641
|
+
}
|
|
4642
|
+
console.log(chalk10.bold("\nDefault Workspace\n"));
|
|
4643
|
+
console.log(`${chalk10.cyan("ID:")} ${ws.workspaceId}`);
|
|
4644
|
+
console.log(`${chalk10.cyan("Name:")} ${ws.name}`);
|
|
4645
|
+
console.log(`${chalk10.cyan("User ID:")} ${ws.userId}`);
|
|
4646
|
+
console.log(`${chalk10.cyan("Created:")} ${new Date(ws.createdAt).toLocaleString()}`);
|
|
4647
|
+
console.log(`${chalk10.cyan("Updated:")} ${new Date(ws.updatedAt).toLocaleString()}`);
|
|
4648
|
+
} catch (error) {
|
|
4649
|
+
exitWithError(error);
|
|
4650
|
+
}
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
// src/commands/index.ts
|
|
4655
|
+
function registerCommands(program2) {
|
|
4656
|
+
program2.command("health").description("Check CLI health").action(async () => {
|
|
4657
|
+
console.log("bisque CLI v0.1.0 - OK");
|
|
4658
|
+
});
|
|
4659
|
+
registerAuthCommand(program2);
|
|
4660
|
+
registerConfigCommand(program2);
|
|
4661
|
+
registerWorkspaceCommand(program2);
|
|
4662
|
+
registerAddCommand(program2);
|
|
4663
|
+
registerGetCommand(program2);
|
|
4664
|
+
registerDeleteCommand(program2);
|
|
4665
|
+
registerBulkAddCommand(program2);
|
|
4666
|
+
registerBatchCommand(program2);
|
|
4667
|
+
registerSearchCommand(program2);
|
|
4668
|
+
registerMoveCommand(program2);
|
|
4669
|
+
registerLinkCommand(program2);
|
|
4670
|
+
registerTagCommand(program2);
|
|
4671
|
+
registerInboxCommand(program2);
|
|
4672
|
+
registerManifestCommand(program2);
|
|
4673
|
+
registerRecallCommand(program2);
|
|
4674
|
+
registerRecallAnnotateCommand(program2);
|
|
4675
|
+
registerUpdateCommand(program2);
|
|
4676
|
+
registerListCommand(program2);
|
|
4677
|
+
registerLoadCommand(program2);
|
|
4678
|
+
registerLsCommand(program2);
|
|
4679
|
+
registerTaskCommand(program2);
|
|
4680
|
+
registerChainCommand(program2);
|
|
4681
|
+
registerTreeCommand(program2);
|
|
4682
|
+
registerMaintenanceCommand(program2);
|
|
4683
|
+
registerPersonaCommand(program2);
|
|
4684
|
+
registerAgentsCommand(program2);
|
|
4685
|
+
registerPluginCommand(program2);
|
|
4686
|
+
registerSkillCommand(program2);
|
|
4687
|
+
registerMeasureCommand(program2);
|
|
4688
|
+
}
|
|
4689
|
+
|
|
4690
|
+
// src/index.ts
|
|
4691
|
+
init_config();
|
|
4692
|
+
init_api_client();
|
|
4693
|
+
init_runtime_context();
|
|
4694
|
+
init_workspace_context();
|
|
4695
|
+
var require2 = createRequire(import.meta.url);
|
|
4696
|
+
var { version } = require2("../package.json");
|
|
4697
|
+
var program = new Command();
|
|
4698
|
+
program.name("bisque").description("Agent memory layer CLI").version(version);
|
|
4699
|
+
program.addOption(
|
|
4700
|
+
new Option3(
|
|
4701
|
+
"--workspace <id>",
|
|
4702
|
+
"Workspace ID to target (overrides BISQUE_WORKSPACE_ID and config)"
|
|
4703
|
+
)
|
|
4704
|
+
).addOption(
|
|
4705
|
+
new Option3(
|
|
4706
|
+
"--no-workspace-header",
|
|
4707
|
+
"Suppress the [workspace: \u2026] header on stdout (still emitted to stderr for agent scraping)"
|
|
4708
|
+
)
|
|
4709
|
+
).addOption(new Option3("--quiet", "Suppress workspace headers and storage-attribution notes"));
|
|
4710
|
+
var ALWAYS_JSON_COMMANDS = /* @__PURE__ */ new Set(["manifest"]);
|
|
4711
|
+
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
4712
|
+
const opts = thisCommand.opts();
|
|
4713
|
+
const headerEnabled = opts.workspaceHeader !== false;
|
|
4714
|
+
const quiet = opts.quiet === true;
|
|
4715
|
+
const actionOpts = actionCommand?.opts?.() ?? {};
|
|
4716
|
+
const explicitJsonFlag = actionOpts.json === true;
|
|
4717
|
+
const actionName = actionCommand?.name?.() ?? "";
|
|
4718
|
+
const alwaysJson = ALWAYS_JSON_COMMANDS.has(actionName);
|
|
4719
|
+
const jsonMode = explicitJsonFlag || alwaysJson;
|
|
4720
|
+
const config = new Config();
|
|
4721
|
+
const workspaceContext = await resolveWorkspaceContext(config, opts.workspace);
|
|
4722
|
+
setRuntimeContext({ workspaceContext, headerEnabled, quiet, config });
|
|
4723
|
+
const effectiveHeaderEnabled = jsonMode ? false : headerEnabled;
|
|
4724
|
+
printWorkspaceHeader(workspaceContext, { enabled: effectiveHeaderEnabled, quiet });
|
|
4725
|
+
});
|
|
4726
|
+
program.hook("postAction", async () => {
|
|
4727
|
+
if (!consumeMembershipMutationFlag()) return;
|
|
4728
|
+
try {
|
|
4729
|
+
await refreshWorkspacesCache(new Config());
|
|
4730
|
+
} catch {
|
|
4731
|
+
}
|
|
4732
|
+
});
|
|
4733
|
+
registerCommands(program);
|
|
4734
|
+
program.parseAsync().catch((err) => {
|
|
4735
|
+
console.error(err.message);
|
|
4736
|
+
process.exit(1);
|
|
4737
|
+
});
|