agent-handoff 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +40 -0
- package/dist/index.js +1429 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { createRequire } from "module";
|
|
5
|
-
import { Command as
|
|
5
|
+
import { Command as Command13 } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/cli/commands/init.ts
|
|
8
8
|
import { Command } from "commander";
|
|
@@ -178,6 +178,14 @@ async function detectStepOutputs(workspacePath, workflow) {
|
|
|
178
178
|
}
|
|
179
179
|
return outputs;
|
|
180
180
|
}
|
|
181
|
+
async function fileExists(filePath) {
|
|
182
|
+
try {
|
|
183
|
+
await fs3.access(filePath);
|
|
184
|
+
return true;
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
181
189
|
|
|
182
190
|
// src/core/state-machine.ts
|
|
183
191
|
function computeState(workflow, stepOutputs) {
|
|
@@ -1245,10 +1253,1424 @@ var reportCommand = new Command7("report").description("\u751F\u6210\u81EA\u52A8
|
|
|
1245
1253
|
}
|
|
1246
1254
|
);
|
|
1247
1255
|
|
|
1256
|
+
// src/cli/commands/export.ts
|
|
1257
|
+
import { Command as Command8 } from "commander";
|
|
1258
|
+
import path13 from "path";
|
|
1259
|
+
|
|
1260
|
+
// src/export/web/exporter.ts
|
|
1261
|
+
import fs11 from "fs/promises";
|
|
1262
|
+
import path12 from "path";
|
|
1263
|
+
|
|
1264
|
+
// src/core/events-reader.ts
|
|
1265
|
+
import fs10 from "fs/promises";
|
|
1266
|
+
import path11 from "path";
|
|
1267
|
+
async function readEventsJsonl(options) {
|
|
1268
|
+
const eventsPath = path11.join(options.workspacePath, "events.jsonl");
|
|
1269
|
+
try {
|
|
1270
|
+
const content = await fs10.readFile(eventsPath, "utf-8");
|
|
1271
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
1272
|
+
const events = [];
|
|
1273
|
+
let invalidLines = 0;
|
|
1274
|
+
for (const line of lines) {
|
|
1275
|
+
try {
|
|
1276
|
+
const event = JSON.parse(line);
|
|
1277
|
+
events.push(event);
|
|
1278
|
+
} catch {
|
|
1279
|
+
invalidLines += 1;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
events.sort((a, b) => {
|
|
1283
|
+
const ats = a.ts ?? "";
|
|
1284
|
+
const bts = b.ts ?? "";
|
|
1285
|
+
if (ats < bts) return -1;
|
|
1286
|
+
if (ats > bts) return 1;
|
|
1287
|
+
return 0;
|
|
1288
|
+
});
|
|
1289
|
+
const limited = typeof options.limit === "number" && options.limit > 0 ? events.slice(-options.limit) : events;
|
|
1290
|
+
return { events: limited, invalidLines };
|
|
1291
|
+
} catch {
|
|
1292
|
+
return { events: [], invalidLines: 0 };
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
function toTimelineItems(events) {
|
|
1296
|
+
return events.map((event) => ({
|
|
1297
|
+
ts: event.ts,
|
|
1298
|
+
stepId: event.step.id,
|
|
1299
|
+
stepIndex: event.step.index,
|
|
1300
|
+
type: event.type,
|
|
1301
|
+
summary: event.summary,
|
|
1302
|
+
...event.workItemId && { workItemId: event.workItemId },
|
|
1303
|
+
...event.links && event.links.length > 0 && { links: event.links },
|
|
1304
|
+
...event.data && { data: event.data }
|
|
1305
|
+
}));
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// src/export/web/artifact-indexer.ts
|
|
1309
|
+
import crypto from "crypto";
|
|
1310
|
+
function sanitizeSegments(segments) {
|
|
1311
|
+
const result = [];
|
|
1312
|
+
for (const seg of segments) {
|
|
1313
|
+
const trimmed = seg.trim();
|
|
1314
|
+
if (!trimmed || trimmed === ".") continue;
|
|
1315
|
+
if (trimmed === "..") {
|
|
1316
|
+
result.push("__");
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
result.push(trimmed);
|
|
1320
|
+
}
|
|
1321
|
+
return result;
|
|
1322
|
+
}
|
|
1323
|
+
function toSafeFilename(original) {
|
|
1324
|
+
const rawSegments = original.split(/[\\/]+/g);
|
|
1325
|
+
const safeSegments = sanitizeSegments(rawSegments);
|
|
1326
|
+
const base = safeSegments.join("-") || "artifact";
|
|
1327
|
+
const normalized = base.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
1328
|
+
const suffix = ".html";
|
|
1329
|
+
const maxLen = 180;
|
|
1330
|
+
if (normalized.length + suffix.length <= maxLen) {
|
|
1331
|
+
return normalized + suffix;
|
|
1332
|
+
}
|
|
1333
|
+
const hash = crypto.createHash("sha1").update(original).digest("hex").slice(0, 10);
|
|
1334
|
+
const headLen = Math.max(16, maxLen - suffix.length - 1 - hash.length);
|
|
1335
|
+
const head = normalized.slice(0, headLen);
|
|
1336
|
+
return `${head}-${hash}${suffix}`;
|
|
1337
|
+
}
|
|
1338
|
+
function buildArtifactLinkMap(links) {
|
|
1339
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1340
|
+
const mappings = [];
|
|
1341
|
+
for (const link of links) {
|
|
1342
|
+
const original = String(link);
|
|
1343
|
+
if (seen.has(original)) continue;
|
|
1344
|
+
seen.add(original);
|
|
1345
|
+
const filename = toSafeFilename(original);
|
|
1346
|
+
const outputHtmlPath = `artifacts/${filename}`;
|
|
1347
|
+
mappings.push({ original, outputHtmlPath, title: original });
|
|
1348
|
+
}
|
|
1349
|
+
return mappings;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// src/export/web/artifact-renderer.ts
|
|
1353
|
+
function escapeHtml(input) {
|
|
1354
|
+
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1355
|
+
}
|
|
1356
|
+
function renderArtifactPage(options) {
|
|
1357
|
+
const title = escapeHtml(options.title);
|
|
1358
|
+
const originalPath = escapeHtml(options.originalPath);
|
|
1359
|
+
const content = escapeHtml(options.content);
|
|
1360
|
+
return [
|
|
1361
|
+
"<!doctype html>",
|
|
1362
|
+
'<html lang="zh-CN">',
|
|
1363
|
+
"<head>",
|
|
1364
|
+
'<meta charset="utf-8" />',
|
|
1365
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
1366
|
+
`<title>${title}</title>`,
|
|
1367
|
+
"<style>",
|
|
1368
|
+
'body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial; color: #111827; background: #ffffff; }',
|
|
1369
|
+
".container { max-width: 1040px; margin: 0 auto; padding: 24px; }",
|
|
1370
|
+
"h1 { font-size: 18px; margin: 0 0 8px; }",
|
|
1371
|
+
'.path { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; color: #374151; }',
|
|
1372
|
+
"pre { margin-top: 16px; background: #0b1020; color: #e5e7eb; padding: 12px; border-radius: 10px; overflow: auto; font-size: 12px; line-height: 1.5; }",
|
|
1373
|
+
"</style>",
|
|
1374
|
+
"</head>",
|
|
1375
|
+
"<body>",
|
|
1376
|
+
'<div class="container">',
|
|
1377
|
+
`<h1>${title}</h1>`,
|
|
1378
|
+
`<div class="path">${originalPath}</div>`,
|
|
1379
|
+
`<pre>${content}</pre>`,
|
|
1380
|
+
"</div>",
|
|
1381
|
+
"</body>",
|
|
1382
|
+
"</html>",
|
|
1383
|
+
""
|
|
1384
|
+
].join("\n");
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// src/export/web/timeline-renderer.ts
|
|
1388
|
+
function escapeHtml2(input) {
|
|
1389
|
+
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1390
|
+
}
|
|
1391
|
+
function renderLinks(links, linkHrefMap) {
|
|
1392
|
+
const items = links.map((link) => {
|
|
1393
|
+
const href = linkHrefMap && linkHrefMap[link] ? linkHrefMap[link] : link;
|
|
1394
|
+
const safeLabel = escapeHtml2(link);
|
|
1395
|
+
const safeHref = escapeHtml2(href);
|
|
1396
|
+
return `<a href="${safeHref}" data-link="${safeLabel}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`;
|
|
1397
|
+
}).join("");
|
|
1398
|
+
return `<div class="links">${items}</div>`;
|
|
1399
|
+
}
|
|
1400
|
+
function renderData(data) {
|
|
1401
|
+
const json = escapeHtml2(JSON.stringify(data, null, 2));
|
|
1402
|
+
return `<details><summary>data</summary><pre>${json}</pre></details>`;
|
|
1403
|
+
}
|
|
1404
|
+
function renderWebTimeline(options) {
|
|
1405
|
+
const assets = {};
|
|
1406
|
+
if (options.includeAssets) {
|
|
1407
|
+
assets["assets/viewer.css"] = getDefaultCss();
|
|
1408
|
+
assets["assets/viewer.js"] = getDefaultJs();
|
|
1409
|
+
}
|
|
1410
|
+
const title = escapeHtml2(options.title);
|
|
1411
|
+
const workspaceName = escapeHtml2(options.workspaceName);
|
|
1412
|
+
const generatedAt = escapeHtml2(options.generatedAt);
|
|
1413
|
+
const eventsJson = escapeHtml2(JSON.stringify(options.items));
|
|
1414
|
+
const linkMapJson = escapeHtml2(JSON.stringify(options.linkHrefMap || {}));
|
|
1415
|
+
const headAssets = options.includeAssets ? `<link rel="stylesheet" href="assets/viewer.css" />` : "";
|
|
1416
|
+
const bodyAssets = options.includeAssets ? `<script src="assets/viewer.js"></script>` : "";
|
|
1417
|
+
const list = options.items.map((item) => {
|
|
1418
|
+
const ts = escapeHtml2(item.ts);
|
|
1419
|
+
const stepId = escapeHtml2(item.stepId);
|
|
1420
|
+
const type = escapeHtml2(item.type);
|
|
1421
|
+
const summary = escapeHtml2(item.summary);
|
|
1422
|
+
const workItem = item.workItemId ? `<span class="badge">${escapeHtml2(item.workItemId)}</span>` : "";
|
|
1423
|
+
const links = item.links && item.links.length > 0 ? renderLinks(item.links, options.linkHrefMap) : "";
|
|
1424
|
+
const data = item.data ? renderData(item.data) : "";
|
|
1425
|
+
return [
|
|
1426
|
+
`<div class="item" data-step="${stepId}" data-type="${type}">`,
|
|
1427
|
+
`<div class="row">`,
|
|
1428
|
+
`<span class="ts">${ts}</span>`,
|
|
1429
|
+
`<span class="badge">${stepId}</span>`,
|
|
1430
|
+
`<span class="type">${type}</span>`,
|
|
1431
|
+
workItem,
|
|
1432
|
+
`</div>`,
|
|
1433
|
+
`<div class="summary">${summary}</div>`,
|
|
1434
|
+
links,
|
|
1435
|
+
data,
|
|
1436
|
+
`</div>`
|
|
1437
|
+
].join("");
|
|
1438
|
+
}).join("");
|
|
1439
|
+
const html = [
|
|
1440
|
+
"<!doctype html>",
|
|
1441
|
+
'<html lang="zh-CN">',
|
|
1442
|
+
"<head>",
|
|
1443
|
+
'<meta charset="utf-8" />',
|
|
1444
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
1445
|
+
`<title>${title}</title>`,
|
|
1446
|
+
headAssets,
|
|
1447
|
+
"</head>",
|
|
1448
|
+
"<body>",
|
|
1449
|
+
'<div class="container">',
|
|
1450
|
+
"<header>",
|
|
1451
|
+
`<h1>${workspaceName} Timeline</h1>`,
|
|
1452
|
+
`<div class="meta">Generated at ${generatedAt}</div>`,
|
|
1453
|
+
"</header>",
|
|
1454
|
+
'<section class="filters" id="timeline-filters">',
|
|
1455
|
+
'<div class="filters-row">',
|
|
1456
|
+
'<input id="filter-q" type="search" placeholder="\u641C\u7D22 summary / data..." />',
|
|
1457
|
+
'<select id="filter-step"></select>',
|
|
1458
|
+
'<select id="filter-type"></select>',
|
|
1459
|
+
'<select id="filter-workItem"></select>',
|
|
1460
|
+
'<button id="filter-clear" type="button">\u6E05\u7A7A\u7B5B\u9009</button>',
|
|
1461
|
+
"</div>",
|
|
1462
|
+
'<div class="filters-meta" id="filters-meta"></div>',
|
|
1463
|
+
"</section>",
|
|
1464
|
+
`<script id="__EVENTS__" type="application/json">${eventsJson}</script>`,
|
|
1465
|
+
`<script id="__LINK_HREF_MAP__" type="application/json">${linkMapJson}</script>`,
|
|
1466
|
+
`<div class="list" id="timeline-list">${list}</div>`,
|
|
1467
|
+
"</div>",
|
|
1468
|
+
bodyAssets,
|
|
1469
|
+
"</body>",
|
|
1470
|
+
"</html>"
|
|
1471
|
+
].filter((x) => x !== "").join("\n");
|
|
1472
|
+
return { html, assets };
|
|
1473
|
+
}
|
|
1474
|
+
function getDefaultCss() {
|
|
1475
|
+
return [
|
|
1476
|
+
"* { box-sizing: border-box; }",
|
|
1477
|
+
'body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; color: #111827; background: #ffffff; }',
|
|
1478
|
+
".container { max-width: 1040px; margin: 0 auto; padding: 24px; }",
|
|
1479
|
+
"header { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; border-bottom: 1px solid #e5e7eb; padding-bottom: 12px; margin-bottom: 16px; }",
|
|
1480
|
+
"h1 { font-size: 20px; margin: 0; }",
|
|
1481
|
+
".meta { font-size: 12px; color: #6b7280; }",
|
|
1482
|
+
".filters { margin: 12px 0 16px; padding: 12px; border: 1px solid #e5e7eb; border-radius: 10px; background: #fafafa; }",
|
|
1483
|
+
".filters-row { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr auto; gap: 10px; align-items: center; }",
|
|
1484
|
+
".filters-row input, .filters-row select { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid #d1d5db; font-size: 13px; background: #fff; }",
|
|
1485
|
+
".filters-row button { padding: 8px 12px; border-radius: 8px; border: 1px solid #d1d5db; background: #fff; cursor: pointer; }",
|
|
1486
|
+
".filters-row button:hover { background: #f3f4f6; }",
|
|
1487
|
+
".filters-meta { margin-top: 8px; font-size: 12px; color: #6b7280; }",
|
|
1488
|
+
".list { display: flex; flex-direction: column; gap: 10px; }",
|
|
1489
|
+
".item { border: 1px solid #e5e7eb; border-radius: 10px; padding: 12px; }",
|
|
1490
|
+
".row { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; }",
|
|
1491
|
+
'.ts { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; color: #374151; }',
|
|
1492
|
+
".badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px; font-size: 12px; background: #eff6ff; color: #1d4ed8; border: 1px solid #dbeafe; }",
|
|
1493
|
+
'.type { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; color: #111827; background: #f3f4f6; padding: 2px 8px; border-radius: 8px; }',
|
|
1494
|
+
".summary { margin-top: 8px; white-space: pre-wrap; line-height: 1.45; }",
|
|
1495
|
+
".links { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }",
|
|
1496
|
+
".links a { font-size: 12px; color: #2563eb; text-decoration: none; border-bottom: 1px dashed #93c5fd; }",
|
|
1497
|
+
".links a:hover { border-bottom-style: solid; }",
|
|
1498
|
+
"details { margin-top: 10px; }",
|
|
1499
|
+
"pre { background: #0b1020; color: #e5e7eb; padding: 10px; border-radius: 10px; overflow: auto; font-size: 12px; line-height: 1.5; }",
|
|
1500
|
+
""
|
|
1501
|
+
].join("\n");
|
|
1502
|
+
}
|
|
1503
|
+
function getDefaultJs() {
|
|
1504
|
+
return [
|
|
1505
|
+
"(() => {",
|
|
1506
|
+
" const g = globalThis;",
|
|
1507
|
+
" const doc = g.document;",
|
|
1508
|
+
' if (!doc || typeof doc.getElementById !== "function") return;',
|
|
1509
|
+
' const el = doc.getElementById("__EVENTS__");',
|
|
1510
|
+
" if (!el) return;",
|
|
1511
|
+
' const qInput = doc.getElementById("filter-q");',
|
|
1512
|
+
' const stepSelect = doc.getElementById("filter-step");',
|
|
1513
|
+
' const typeSelect = doc.getElementById("filter-type");',
|
|
1514
|
+
' const workItemSelect = doc.getElementById("filter-workItem");',
|
|
1515
|
+
' const clearBtn = doc.getElementById("filter-clear");',
|
|
1516
|
+
' const listEl = doc.getElementById("timeline-list");',
|
|
1517
|
+
' const metaEl = doc.getElementById("filters-meta");',
|
|
1518
|
+
" const HTMLInput = g.HTMLInputElement;",
|
|
1519
|
+
" const HTMLSelect = g.HTMLSelectElement;",
|
|
1520
|
+
" const HTMLButton = g.HTMLButtonElement;",
|
|
1521
|
+
" const HTMLElementCtor = g.HTMLElement;",
|
|
1522
|
+
" if (!HTMLInput || !HTMLSelect || !HTMLButton || !HTMLElementCtor) return;",
|
|
1523
|
+
" if (",
|
|
1524
|
+
" !(qInput instanceof HTMLInput) ||",
|
|
1525
|
+
" !(stepSelect instanceof HTMLSelect) ||",
|
|
1526
|
+
" !(typeSelect instanceof HTMLSelect) ||",
|
|
1527
|
+
" !(workItemSelect instanceof HTMLSelect) ||",
|
|
1528
|
+
" !(clearBtn instanceof HTMLButton) ||",
|
|
1529
|
+
" !(listEl instanceof HTMLElementCtor)",
|
|
1530
|
+
" ) {",
|
|
1531
|
+
" return;",
|
|
1532
|
+
" }",
|
|
1533
|
+
" let events;",
|
|
1534
|
+
" try {",
|
|
1535
|
+
' events = JSON.parse(el.textContent || "[]");',
|
|
1536
|
+
" } catch {",
|
|
1537
|
+
" return;",
|
|
1538
|
+
" }",
|
|
1539
|
+
' const linkMapEl = doc.getElementById("__LINK_HREF_MAP__");',
|
|
1540
|
+
" let linkHrefMap = {};",
|
|
1541
|
+
' if (linkMapEl && typeof linkMapEl.textContent === "string" && linkMapEl.textContent.trim()) {',
|
|
1542
|
+
" try {",
|
|
1543
|
+
" linkHrefMap = JSON.parse(linkMapEl.textContent) || {};",
|
|
1544
|
+
" } catch {",
|
|
1545
|
+
" linkHrefMap = {};",
|
|
1546
|
+
" }",
|
|
1547
|
+
" }",
|
|
1548
|
+
' const safeText = (v) => (v == null ? "" : String(v));',
|
|
1549
|
+
" const normalize = (v) => safeText(v).toLowerCase();",
|
|
1550
|
+
" const getUniqueSorted = (arr) =>",
|
|
1551
|
+
' Array.from(new Set(arr.filter((x) => x != null && String(x).trim() !== "").map(String))).sort();',
|
|
1552
|
+
" const steps = getUniqueSorted(events.map((e) => e.stepId));",
|
|
1553
|
+
" const types = getUniqueSorted(events.map((e) => e.type));",
|
|
1554
|
+
" const workItems = getUniqueSorted(events.map((e) => e.workItemId));",
|
|
1555
|
+
" const setOptions = (select, values, labelAll) => {",
|
|
1556
|
+
' select.innerHTML = "";',
|
|
1557
|
+
' const optAll = doc.createElement("option");',
|
|
1558
|
+
' optAll.value = "";',
|
|
1559
|
+
" optAll.textContent = labelAll;",
|
|
1560
|
+
" select.appendChild(optAll);",
|
|
1561
|
+
" for (const v of values) {",
|
|
1562
|
+
' const opt = doc.createElement("option");',
|
|
1563
|
+
" opt.value = v;",
|
|
1564
|
+
" opt.textContent = v;",
|
|
1565
|
+
" select.appendChild(opt);",
|
|
1566
|
+
" }",
|
|
1567
|
+
" };",
|
|
1568
|
+
' setOptions(stepSelect, steps, "\u5168\u90E8 step");',
|
|
1569
|
+
' setOptions(typeSelect, types, "\u5168\u90E8 type");',
|
|
1570
|
+
' setOptions(workItemSelect, workItems, "\u5168\u90E8 workItem");',
|
|
1571
|
+
' const getQuery = () => new URLSearchParams(g.location ? g.location.search : "");',
|
|
1572
|
+
" const applyFromUrl = () => {",
|
|
1573
|
+
" const qs = getQuery();",
|
|
1574
|
+
' qInput.value = safeText(qs.get("q") || "");',
|
|
1575
|
+
' stepSelect.value = safeText(qs.get("step") || "");',
|
|
1576
|
+
' typeSelect.value = safeText(qs.get("type") || "");',
|
|
1577
|
+
' workItemSelect.value = safeText(qs.get("workItem") || "");',
|
|
1578
|
+
" };",
|
|
1579
|
+
" const writeUrl = () => {",
|
|
1580
|
+
" if (!g.history || !g.location) return;",
|
|
1581
|
+
" const qs = new URLSearchParams();",
|
|
1582
|
+
' if (qInput.value.trim()) qs.set("q", qInput.value.trim());',
|
|
1583
|
+
' if (stepSelect.value) qs.set("step", stepSelect.value);',
|
|
1584
|
+
' if (typeSelect.value) qs.set("type", typeSelect.value);',
|
|
1585
|
+
' if (workItemSelect.value) qs.set("workItem", workItemSelect.value);',
|
|
1586
|
+
" const qsStr = qs.toString();",
|
|
1587
|
+
" const next = qsStr ? `${g.location.pathname}?${qsStr}` : g.location.pathname;",
|
|
1588
|
+
' g.history.replaceState(null, "", next);',
|
|
1589
|
+
" };",
|
|
1590
|
+
" const escapeHtml = (input) =>",
|
|
1591
|
+
" safeText(input)",
|
|
1592
|
+
' .replaceAll("&", "&")',
|
|
1593
|
+
' .replaceAll("<", "<")',
|
|
1594
|
+
' .replaceAll(">", ">")',
|
|
1595
|
+
' .replaceAll("\\"", """)',
|
|
1596
|
+
` .replaceAll("\\'", "'");`,
|
|
1597
|
+
" const renderLinks = (links) => {",
|
|
1598
|
+
' if (!Array.isArray(links) || links.length === 0) return "";',
|
|
1599
|
+
" const html = links",
|
|
1600
|
+
" .map((l) => {",
|
|
1601
|
+
" const href = linkHrefMap && linkHrefMap[l] ? linkHrefMap[l] : l;",
|
|
1602
|
+
" const safeHref = escapeHtml(href);",
|
|
1603
|
+
" const label = escapeHtml(l);",
|
|
1604
|
+
' return `<a href="${safeHref}" target="_blank" rel="noopener noreferrer">${label}</a>`;',
|
|
1605
|
+
" })",
|
|
1606
|
+
' .join("");',
|
|
1607
|
+
' return `<div class="links">${html}</div>`;',
|
|
1608
|
+
" };",
|
|
1609
|
+
" const renderData = (data) => {",
|
|
1610
|
+
' if (!data) return "";',
|
|
1611
|
+
' let json = "";',
|
|
1612
|
+
" try {",
|
|
1613
|
+
" json = JSON.stringify(data, null, 2);",
|
|
1614
|
+
" } catch {",
|
|
1615
|
+
" json = safeText(data);",
|
|
1616
|
+
" }",
|
|
1617
|
+
" return `<details><summary>data</summary><pre>${escapeHtml(json)}</pre></details>`;",
|
|
1618
|
+
" };",
|
|
1619
|
+
" const renderList = (items) => {",
|
|
1620
|
+
" listEl.innerHTML = items",
|
|
1621
|
+
" .map((item) => {",
|
|
1622
|
+
" const ts = escapeHtml(item.ts);",
|
|
1623
|
+
" const stepId = escapeHtml(item.stepId);",
|
|
1624
|
+
" const type = escapeHtml(item.type);",
|
|
1625
|
+
" const summary = escapeHtml(item.summary);",
|
|
1626
|
+
' const workItem = item.workItemId ? `<span class="badge">${escapeHtml(item.workItemId)}</span>` : "";',
|
|
1627
|
+
" return [",
|
|
1628
|
+
' `<div class="item" data-step="${stepId}" data-type="${type}">`,',
|
|
1629
|
+
' `<div class="row">`,',
|
|
1630
|
+
' `<span class="ts">${ts}</span>`,',
|
|
1631
|
+
' `<span class="badge">${stepId}</span>`,',
|
|
1632
|
+
' `<span class="type">${type}</span>`,',
|
|
1633
|
+
" workItem,",
|
|
1634
|
+
" `</div>`,",
|
|
1635
|
+
' `<div class="summary">${summary}</div>`,',
|
|
1636
|
+
" renderLinks(item.links),",
|
|
1637
|
+
" renderData(item.data),",
|
|
1638
|
+
" `</div>`,",
|
|
1639
|
+
' ].join("");',
|
|
1640
|
+
" })",
|
|
1641
|
+
' .join("");',
|
|
1642
|
+
" };",
|
|
1643
|
+
" const applyFilters = () => {",
|
|
1644
|
+
" const q = normalize(qInput.value.trim());",
|
|
1645
|
+
" const step = safeText(stepSelect.value);",
|
|
1646
|
+
" const type = safeText(typeSelect.value);",
|
|
1647
|
+
" const workItem = safeText(workItemSelect.value);",
|
|
1648
|
+
" const filtered = events.filter((e) => {",
|
|
1649
|
+
" if (step && safeText(e.stepId) !== step) return false;",
|
|
1650
|
+
" if (type && safeText(e.type) !== type) return false;",
|
|
1651
|
+
" if (workItem && safeText(e.workItemId) !== workItem) return false;",
|
|
1652
|
+
" if (!q) return true;",
|
|
1653
|
+
" const hay1 = normalize(e.summary);",
|
|
1654
|
+
' let hay2 = "";',
|
|
1655
|
+
" try {",
|
|
1656
|
+
" hay2 = normalize(JSON.stringify(e.data || {}));",
|
|
1657
|
+
" } catch {",
|
|
1658
|
+
' hay2 = normalize(String(e.data || ""));',
|
|
1659
|
+
" }",
|
|
1660
|
+
" return hay1.includes(q) || hay2.includes(q);",
|
|
1661
|
+
" });",
|
|
1662
|
+
" renderList(filtered);",
|
|
1663
|
+
" if (metaEl) {",
|
|
1664
|
+
" metaEl.textContent = `\u663E\u793A ${filtered.length} / ${events.length}`;",
|
|
1665
|
+
" }",
|
|
1666
|
+
" writeUrl();",
|
|
1667
|
+
" };",
|
|
1668
|
+
" applyFromUrl();",
|
|
1669
|
+
" applyFilters();",
|
|
1670
|
+
' qInput.addEventListener("input", applyFilters);',
|
|
1671
|
+
' stepSelect.addEventListener("change", applyFilters);',
|
|
1672
|
+
' typeSelect.addEventListener("change", applyFilters);',
|
|
1673
|
+
' workItemSelect.addEventListener("change", applyFilters);',
|
|
1674
|
+
' clearBtn.addEventListener("click", () => {',
|
|
1675
|
+
' qInput.value = "";',
|
|
1676
|
+
' stepSelect.value = "";',
|
|
1677
|
+
' typeSelect.value = "";',
|
|
1678
|
+
' workItemSelect.value = "";',
|
|
1679
|
+
" applyFilters();",
|
|
1680
|
+
" });",
|
|
1681
|
+
"})();",
|
|
1682
|
+
""
|
|
1683
|
+
].join("\n");
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// src/export/web/exporter.ts
|
|
1687
|
+
async function ensureDir(dir) {
|
|
1688
|
+
await fs11.mkdir(dir, { recursive: true });
|
|
1689
|
+
}
|
|
1690
|
+
async function exportWebTimeline(options) {
|
|
1691
|
+
const { events, invalidLines } = await readEventsJsonl({
|
|
1692
|
+
workspacePath: options.workspacePath,
|
|
1693
|
+
limit: options.limit
|
|
1694
|
+
});
|
|
1695
|
+
const items = toTimelineItems(events);
|
|
1696
|
+
const linkSet = /* @__PURE__ */ new Set();
|
|
1697
|
+
for (const item of items) {
|
|
1698
|
+
for (const link of item.links || []) {
|
|
1699
|
+
linkSet.add(link);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
const mappings = buildArtifactLinkMap([...linkSet]);
|
|
1703
|
+
const linkHrefMap = {};
|
|
1704
|
+
for (const m of mappings) {
|
|
1705
|
+
linkHrefMap[m.original] = m.outputHtmlPath;
|
|
1706
|
+
}
|
|
1707
|
+
const out = path12.resolve(options.outputDir);
|
|
1708
|
+
await ensureDir(out);
|
|
1709
|
+
await ensureDir(path12.join(out, "assets"));
|
|
1710
|
+
await ensureDir(path12.join(out, "artifacts"));
|
|
1711
|
+
for (const m of mappings) {
|
|
1712
|
+
const artifactSourcePath = path12.join(options.workspacePath, m.original);
|
|
1713
|
+
let content;
|
|
1714
|
+
try {
|
|
1715
|
+
content = await fs11.readFile(artifactSourcePath, "utf-8");
|
|
1716
|
+
} catch {
|
|
1717
|
+
content = `\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BFB\u53D6: ${m.original}`;
|
|
1718
|
+
}
|
|
1719
|
+
const html = renderArtifactPage({
|
|
1720
|
+
title: m.title,
|
|
1721
|
+
originalPath: m.original,
|
|
1722
|
+
content
|
|
1723
|
+
});
|
|
1724
|
+
const fullOutPath = path12.join(out, m.outputHtmlPath);
|
|
1725
|
+
await ensureDir(path12.dirname(fullOutPath));
|
|
1726
|
+
await fs11.writeFile(fullOutPath, html, "utf-8");
|
|
1727
|
+
}
|
|
1728
|
+
const timeline = renderWebTimeline({
|
|
1729
|
+
title: "AgentHandoff Timeline",
|
|
1730
|
+
workspaceName: path12.basename(path12.resolve(options.workspacePath)),
|
|
1731
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1732
|
+
items,
|
|
1733
|
+
includeAssets: true,
|
|
1734
|
+
linkHrefMap
|
|
1735
|
+
});
|
|
1736
|
+
await fs11.writeFile(path12.join(out, "index.html"), timeline.html, "utf-8");
|
|
1737
|
+
for (const [rel, content] of Object.entries(timeline.assets)) {
|
|
1738
|
+
const full = path12.join(out, rel);
|
|
1739
|
+
await ensureDir(path12.dirname(full));
|
|
1740
|
+
await fs11.writeFile(full, content, "utf-8");
|
|
1741
|
+
}
|
|
1742
|
+
return {
|
|
1743
|
+
outputDir: out,
|
|
1744
|
+
indexPath: path12.join(out, "index.html"),
|
|
1745
|
+
eventsCount: items.length,
|
|
1746
|
+
invalidLines,
|
|
1747
|
+
artifactsCount: mappings.length
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// src/cli/commands/export.ts
|
|
1752
|
+
var exportCommand = new Command8("export").description("\u5BFC\u51FA\u9759\u6001\u4EA7\u7269\uFF08\u4F8B\u5982 Web Timeline\uFF09").argument("[workspace]", "workspace \u8DEF\u5F84", ".").requiredOption("--format <format>", "\u5BFC\u51FA\u683C\u5F0F: web", "web").option("-o, --output <dir>", "\u5BFC\u51FA\u76EE\u5F55\uFF08\u9ED8\u8BA4 <workspace>/timeline\uFF09").option("--limit <n>", "\u53EA\u5BFC\u51FA\u6700\u8FD1 n \u6761\u4E8B\u4EF6", (v) => parseInt(v, 10)).action(
|
|
1753
|
+
async (workspace, options) => {
|
|
1754
|
+
const workspacePath = path13.resolve(workspace);
|
|
1755
|
+
try {
|
|
1756
|
+
const info = await loadWorkspace(workspacePath);
|
|
1757
|
+
if (!info.exists) {
|
|
1758
|
+
console.error(`Error: workspace not found: ${workspacePath}`);
|
|
1759
|
+
process.exit(1);
|
|
1760
|
+
}
|
|
1761
|
+
if (!info.hasWorkflow) {
|
|
1762
|
+
console.error(`Error: workflow.yaml not found in ${workspacePath}`);
|
|
1763
|
+
process.exit(1);
|
|
1764
|
+
}
|
|
1765
|
+
if (options.format !== "web") {
|
|
1766
|
+
console.error(`Error: unsupported format: ${options.format}`);
|
|
1767
|
+
process.exit(1);
|
|
1768
|
+
}
|
|
1769
|
+
const outputDir = options.output ? path13.resolve(options.output) : path13.join(workspacePath, "timeline");
|
|
1770
|
+
const result = await exportWebTimeline({
|
|
1771
|
+
workspacePath,
|
|
1772
|
+
outputDir,
|
|
1773
|
+
limit: options.limit
|
|
1774
|
+
});
|
|
1775
|
+
console.log(`\u2705 Exported to: ${result.outputDir}`);
|
|
1776
|
+
console.log(` index: ${result.indexPath}`);
|
|
1777
|
+
console.log(` events: ${result.eventsCount} (invalid lines: ${result.invalidLines})`);
|
|
1778
|
+
console.log(` artifacts: ${result.artifactsCount}`);
|
|
1779
|
+
console.log("");
|
|
1780
|
+
console.log(`\u6253\u5F00\u65B9\u5F0F\uFF08macOS\uFF09\uFF1Aopen ${path13.join(result.outputDir, "index.html")}`);
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
console.error(`Error: ${error}`);
|
|
1783
|
+
process.exit(1);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
);
|
|
1787
|
+
|
|
1788
|
+
// src/cli/commands/index.ts
|
|
1789
|
+
import { Command as Command9 } from "commander";
|
|
1790
|
+
import fs14 from "fs/promises";
|
|
1791
|
+
import path16 from "path";
|
|
1792
|
+
|
|
1793
|
+
// src/core/index/indexer.ts
|
|
1794
|
+
import fs12 from "fs/promises";
|
|
1795
|
+
import path14 from "path";
|
|
1796
|
+
async function readPreview(filePath, maxChars) {
|
|
1797
|
+
try {
|
|
1798
|
+
const content = await fs12.readFile(filePath, "utf-8");
|
|
1799
|
+
const sliced = content.slice(0, maxChars);
|
|
1800
|
+
return sliced;
|
|
1801
|
+
} catch {
|
|
1802
|
+
return void 0;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
async function statInfo(filePath) {
|
|
1806
|
+
try {
|
|
1807
|
+
const st = await fs12.stat(filePath);
|
|
1808
|
+
return { bytes: st.size, updatedAt: st.mtime.toISOString() };
|
|
1809
|
+
} catch {
|
|
1810
|
+
return {};
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
async function addArtifact(workspacePath, relPath, kind) {
|
|
1814
|
+
const full = path14.join(workspacePath, relPath);
|
|
1815
|
+
const exists = await fileExists(full);
|
|
1816
|
+
if (!exists) {
|
|
1817
|
+
return { path: relPath, kind };
|
|
1818
|
+
}
|
|
1819
|
+
const { bytes, updatedAt } = await statInfo(full);
|
|
1820
|
+
const preview = await readPreview(full, 2e3);
|
|
1821
|
+
return { path: relPath, kind, bytes, updatedAt, ...preview !== void 0 && { preview } };
|
|
1822
|
+
}
|
|
1823
|
+
async function buildWorkspaceIndex(workspacePath) {
|
|
1824
|
+
const info = await loadWorkspace(workspacePath);
|
|
1825
|
+
if (!info.exists) {
|
|
1826
|
+
throw new Error(`workspace not found: ${path14.resolve(workspacePath)}`);
|
|
1827
|
+
}
|
|
1828
|
+
if (!info.hasWorkflow || !info.workflow) {
|
|
1829
|
+
throw new Error(`workflow.yaml not found in ${path14.resolve(workspacePath)}`);
|
|
1830
|
+
}
|
|
1831
|
+
const abs = info.path;
|
|
1832
|
+
const indexedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1833
|
+
const workspaceName = path14.basename(abs);
|
|
1834
|
+
const steps = await Promise.all(
|
|
1835
|
+
info.workflow.steps.map(async (step, i) => {
|
|
1836
|
+
const outputRel = step.output;
|
|
1837
|
+
const outputFull = path14.join(abs, outputRel);
|
|
1838
|
+
const outputExists = await fileExists(outputFull);
|
|
1839
|
+
return { id: step.id, index: i + 1, outputPath: outputRel, outputExists };
|
|
1840
|
+
})
|
|
1841
|
+
);
|
|
1842
|
+
const artifacts = [];
|
|
1843
|
+
const briefRel = "brief.md";
|
|
1844
|
+
if (await fileExists(path14.join(abs, briefRel))) {
|
|
1845
|
+
artifacts.push(await addArtifact(abs, briefRel, "brief"));
|
|
1846
|
+
}
|
|
1847
|
+
for (const step of info.workflow.steps) {
|
|
1848
|
+
const rel = step.output;
|
|
1849
|
+
artifacts.push(await addArtifact(abs, rel, "step.output"));
|
|
1850
|
+
}
|
|
1851
|
+
const { events } = await readEventsJsonl({ workspacePath: abs });
|
|
1852
|
+
const items = toTimelineItems(events);
|
|
1853
|
+
const indexedEvents = items.map((item) => ({
|
|
1854
|
+
ts: item.ts,
|
|
1855
|
+
stepId: item.stepId,
|
|
1856
|
+
stepIndex: item.stepIndex,
|
|
1857
|
+
type: item.type,
|
|
1858
|
+
summary: item.summary,
|
|
1859
|
+
...item.workItemId && { workItemId: item.workItemId },
|
|
1860
|
+
...item.links && item.links.length > 0 && { links: item.links }
|
|
1861
|
+
}));
|
|
1862
|
+
return {
|
|
1863
|
+
version: 1,
|
|
1864
|
+
workspacePath: abs,
|
|
1865
|
+
workspaceName,
|
|
1866
|
+
indexedAt,
|
|
1867
|
+
workflowName: info.workflow.name,
|
|
1868
|
+
steps,
|
|
1869
|
+
artifacts,
|
|
1870
|
+
events: indexedEvents
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// src/core/index/registry.ts
|
|
1875
|
+
import fs13 from "fs/promises";
|
|
1876
|
+
import os3 from "os";
|
|
1877
|
+
import path15 from "path";
|
|
1878
|
+
function getHomeDir() {
|
|
1879
|
+
try {
|
|
1880
|
+
const envHome = process.env.HOME;
|
|
1881
|
+
if (envHome && envHome.trim()) return envHome;
|
|
1882
|
+
const home = os3.homedir();
|
|
1883
|
+
if (home && home.trim()) return home;
|
|
1884
|
+
return null;
|
|
1885
|
+
} catch {
|
|
1886
|
+
return null;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
function defaultRegistryFilePath() {
|
|
1890
|
+
const home = getHomeDir();
|
|
1891
|
+
const base = home ? home : process.cwd();
|
|
1892
|
+
return path15.join(base, ".agenthandoff", "registry.json");
|
|
1893
|
+
}
|
|
1894
|
+
async function ensureParentDir(filePath) {
|
|
1895
|
+
await fs13.mkdir(path15.dirname(filePath), { recursive: true });
|
|
1896
|
+
}
|
|
1897
|
+
function normalizeRegistry(registry) {
|
|
1898
|
+
if (typeof registry === "object" && registry !== null && "version" in registry && registry.version === 1 && "items" in registry && Array.isArray(registry.items)) {
|
|
1899
|
+
const items = registry.items.map((item) => {
|
|
1900
|
+
if (typeof item !== "object" || item === null) return null;
|
|
1901
|
+
const obj = item;
|
|
1902
|
+
const name = typeof obj.name === "string" ? obj.name : "";
|
|
1903
|
+
const itemPath = typeof obj.path === "string" ? obj.path : "";
|
|
1904
|
+
const addedAt = typeof obj.addedAt === "string" ? obj.addedAt : "";
|
|
1905
|
+
const updatedAt = typeof obj.updatedAt === "string" ? obj.updatedAt : "";
|
|
1906
|
+
if (!name || !itemPath) return null;
|
|
1907
|
+
return { name, path: itemPath, addedAt, updatedAt };
|
|
1908
|
+
}).filter((x) => Boolean(x));
|
|
1909
|
+
return { version: 1, items };
|
|
1910
|
+
}
|
|
1911
|
+
return { version: 1, items: [] };
|
|
1912
|
+
}
|
|
1913
|
+
function createDefaultRegistryStore() {
|
|
1914
|
+
const filePath = defaultRegistryFilePath();
|
|
1915
|
+
return {
|
|
1916
|
+
async load() {
|
|
1917
|
+
try {
|
|
1918
|
+
const raw = await fs13.readFile(filePath, "utf-8");
|
|
1919
|
+
const parsed = JSON.parse(raw);
|
|
1920
|
+
return normalizeRegistry(parsed);
|
|
1921
|
+
} catch {
|
|
1922
|
+
return { version: 1, items: [] };
|
|
1923
|
+
}
|
|
1924
|
+
},
|
|
1925
|
+
async save(registry) {
|
|
1926
|
+
await ensureParentDir(filePath);
|
|
1927
|
+
const normalized = normalizeRegistry(registry);
|
|
1928
|
+
await fs13.writeFile(filePath, JSON.stringify(normalized, null, 2) + "\n", "utf-8");
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
function defaultNameForPath(workspacePath) {
|
|
1933
|
+
const abs = path15.resolve(workspacePath);
|
|
1934
|
+
return path15.basename(abs) || abs;
|
|
1935
|
+
}
|
|
1936
|
+
async function registerWorkspace(workspacePath, name) {
|
|
1937
|
+
const store = createDefaultRegistryStore();
|
|
1938
|
+
const registry = await store.load();
|
|
1939
|
+
const absPath = path15.resolve(workspacePath);
|
|
1940
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1941
|
+
const itemName = name && name.trim() ? name.trim() : defaultNameForPath(absPath);
|
|
1942
|
+
const existingIndex = registry.items.findIndex((i) => path15.resolve(i.path) === absPath);
|
|
1943
|
+
if (existingIndex >= 0) {
|
|
1944
|
+
const existing = registry.items[existingIndex];
|
|
1945
|
+
registry.items[existingIndex] = {
|
|
1946
|
+
...existing,
|
|
1947
|
+
name: itemName,
|
|
1948
|
+
updatedAt: now,
|
|
1949
|
+
addedAt: existing.addedAt || now,
|
|
1950
|
+
path: absPath
|
|
1951
|
+
};
|
|
1952
|
+
} else {
|
|
1953
|
+
registry.items.push({
|
|
1954
|
+
name: itemName,
|
|
1955
|
+
path: absPath,
|
|
1956
|
+
addedAt: now,
|
|
1957
|
+
updatedAt: now
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
await store.save(registry);
|
|
1961
|
+
}
|
|
1962
|
+
async function unregisterWorkspace(pathOrName) {
|
|
1963
|
+
const store = createDefaultRegistryStore();
|
|
1964
|
+
const registry = await store.load();
|
|
1965
|
+
const token = String(pathOrName).trim();
|
|
1966
|
+
if (!token) return;
|
|
1967
|
+
const abs = path15.resolve(token);
|
|
1968
|
+
registry.items = registry.items.filter((item) => {
|
|
1969
|
+
const itemAbs = path15.resolve(item.path);
|
|
1970
|
+
return itemAbs !== abs && item.name !== token;
|
|
1971
|
+
});
|
|
1972
|
+
await store.save(registry);
|
|
1973
|
+
}
|
|
1974
|
+
async function listWorkspaces() {
|
|
1975
|
+
const store = createDefaultRegistryStore();
|
|
1976
|
+
const registry = await store.load();
|
|
1977
|
+
return [...registry.items].sort((a, b) => a.name.localeCompare(b.name));
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// src/cli/commands/index.ts
|
|
1981
|
+
async function ensureDir2(dir) {
|
|
1982
|
+
await fs14.mkdir(dir, { recursive: true });
|
|
1983
|
+
}
|
|
1984
|
+
var indexCommand = new Command9("index").description("\u751F\u6210 workspace \u7D22\u5F15\uFF0C\u5E76\u652F\u6301 registry \u7BA1\u7406").argument("[workspace]", "workspace \u8DEF\u5F84", ".").option("--add", "\u5C06 workspace \u52A0\u5165 registry").option("--list", "\u5217\u51FA registry \u4E2D\u7684 workspaces").option("--remove <pathOrName>", "\u4ECE registry \u5220\u9664 workspace\uFF08path \u6216 name\uFF09").option("--output <file>", "\u8F93\u51FA\u7D22\u5F15\u6587\u4EF6\u8DEF\u5F84\uFF08\u9ED8\u8BA4 <workspace>/.agenthandoff/index.json\uFF09").option("--json", "\u4EE5 JSON \u8F93\u51FA\u7ED3\u679C\u5230 stdout").action(
|
|
1985
|
+
async (workspace, options) => {
|
|
1986
|
+
try {
|
|
1987
|
+
if (options.list) {
|
|
1988
|
+
const items = await listWorkspaces();
|
|
1989
|
+
if (options.json) {
|
|
1990
|
+
console.log(JSON.stringify(items, null, 2));
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
if (items.length === 0) {
|
|
1994
|
+
console.log("Registry is empty");
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
for (const item of items) {
|
|
1998
|
+
console.log(`${item.name} ${item.path}`);
|
|
1999
|
+
}
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
if (options.remove) {
|
|
2003
|
+
await unregisterWorkspace(options.remove);
|
|
2004
|
+
console.log(`\u2705 Removed from registry: ${options.remove}`);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
const workspacePath = path16.resolve(workspace);
|
|
2008
|
+
const info = await loadWorkspace(workspacePath);
|
|
2009
|
+
if (!info.exists) {
|
|
2010
|
+
console.error(`Error: workspace not found: ${workspacePath}`);
|
|
2011
|
+
process.exit(1);
|
|
2012
|
+
}
|
|
2013
|
+
if (!info.hasWorkflow || !info.workflow) {
|
|
2014
|
+
console.error(`Error: workflow.yaml not found in ${workspacePath}`);
|
|
2015
|
+
process.exit(1);
|
|
2016
|
+
}
|
|
2017
|
+
if (options.add) {
|
|
2018
|
+
await registerWorkspace(workspacePath, info.workflow.name);
|
|
2019
|
+
}
|
|
2020
|
+
const index = await buildWorkspaceIndex(workspacePath);
|
|
2021
|
+
const outputFile = options.output ? path16.resolve(options.output) : path16.join(workspacePath, ".agenthandoff", "index.json");
|
|
2022
|
+
await ensureDir2(path16.dirname(outputFile));
|
|
2023
|
+
await fs14.writeFile(outputFile, JSON.stringify(index, null, 2) + "\n", "utf-8");
|
|
2024
|
+
if (options.json) {
|
|
2025
|
+
console.log(JSON.stringify(index, null, 2));
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
console.log(`\u2705 Indexed: ${workspacePath}`);
|
|
2029
|
+
console.log(` output: ${outputFile}`);
|
|
2030
|
+
console.log(` steps: ${index.steps.length}`);
|
|
2031
|
+
console.log(` artifacts: ${index.artifacts.length}`);
|
|
2032
|
+
console.log(` events: ${index.events.length}`);
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
console.error(`Error: ${error}`);
|
|
2035
|
+
process.exit(1);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
);
|
|
2039
|
+
|
|
2040
|
+
// src/cli/commands/search.ts
|
|
2041
|
+
import { Command as Command10 } from "commander";
|
|
2042
|
+
|
|
2043
|
+
// src/core/search/search.ts
|
|
2044
|
+
import fs15 from "fs/promises";
|
|
2045
|
+
import path17 from "path";
|
|
2046
|
+
async function readIndexFile(workspacePath, indexDir) {
|
|
2047
|
+
const indexFile = indexDir ? path17.join(indexDir, "index.json") : path17.join(workspacePath, ".agenthandoff", "index.json");
|
|
2048
|
+
try {
|
|
2049
|
+
const raw = await fs15.readFile(indexFile, "utf-8");
|
|
2050
|
+
return JSON.parse(raw);
|
|
2051
|
+
} catch {
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function toLower(input) {
|
|
2056
|
+
return input.toLowerCase();
|
|
2057
|
+
}
|
|
2058
|
+
function normalizeList(values) {
|
|
2059
|
+
if (!values || values.length === 0) return void 0;
|
|
2060
|
+
return values.map((v) => v.trim()).filter((v) => v.length > 0);
|
|
2061
|
+
}
|
|
2062
|
+
function textIncludes(haystack, needle) {
|
|
2063
|
+
return toLower(haystack).includes(needle);
|
|
2064
|
+
}
|
|
2065
|
+
function matchOptional(value, filter) {
|
|
2066
|
+
if (!filter || filter.length === 0) return true;
|
|
2067
|
+
if (!value) return false;
|
|
2068
|
+
return filter.includes(value);
|
|
2069
|
+
}
|
|
2070
|
+
function makeSnippet(text, q) {
|
|
2071
|
+
const lower = toLower(text);
|
|
2072
|
+
const idx = lower.indexOf(q);
|
|
2073
|
+
if (idx < 0) {
|
|
2074
|
+
return text.length > 160 ? text.slice(0, 160) : text;
|
|
2075
|
+
}
|
|
2076
|
+
const start = Math.max(0, idx - 40);
|
|
2077
|
+
const end = Math.min(text.length, idx + q.length + 80);
|
|
2078
|
+
return text.slice(start, end);
|
|
2079
|
+
}
|
|
2080
|
+
function scoreHit(hit) {
|
|
2081
|
+
return hit.score;
|
|
2082
|
+
}
|
|
2083
|
+
async function resolveTargets(query, options) {
|
|
2084
|
+
const workspaceFilters = normalizeList(query.workspace);
|
|
2085
|
+
const registryItems = options.registryOnly ? await listWorkspaces() : [];
|
|
2086
|
+
const targets = [];
|
|
2087
|
+
if (registryItems.length > 0) {
|
|
2088
|
+
for (const item of registryItems) {
|
|
2089
|
+
if (!workspaceFilters || workspaceFilters.includes(item.name) || workspaceFilters.includes(item.path)) {
|
|
2090
|
+
targets.push({ name: item.name, path: item.path });
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (!options.registryOnly && workspaceFilters && workspaceFilters.length > 0) {
|
|
2095
|
+
for (const token of workspaceFilters) {
|
|
2096
|
+
const exists = targets.some((t) => t.path === token || t.name === token);
|
|
2097
|
+
if (!exists) {
|
|
2098
|
+
targets.push({ name: path17.basename(token), path: token });
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
return targets;
|
|
2103
|
+
}
|
|
2104
|
+
async function search(query, options) {
|
|
2105
|
+
const q = query.q.trim();
|
|
2106
|
+
const qLower = toLower(q);
|
|
2107
|
+
const stepFilter = normalizeList(query.stepId);
|
|
2108
|
+
const typeFilter = normalizeList(query.type);
|
|
2109
|
+
const workItemFilter = normalizeList(query.workItemId);
|
|
2110
|
+
const limit = query.limit && query.limit > 0 ? query.limit : void 0;
|
|
2111
|
+
const targets = await resolveTargets(query, options);
|
|
2112
|
+
const hits = [];
|
|
2113
|
+
for (const target of targets) {
|
|
2114
|
+
const index = await readIndexFile(target.path, options.indexDir);
|
|
2115
|
+
if (!index) {
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
const workspaceName = index.workspaceName || target.name;
|
|
2119
|
+
const workspacePath = index.workspacePath || target.path;
|
|
2120
|
+
if (options.targets === "events" || options.targets === "all") {
|
|
2121
|
+
const events = index.events || [];
|
|
2122
|
+
for (const event of events) {
|
|
2123
|
+
if (!matchOptional(event.stepId, stepFilter)) continue;
|
|
2124
|
+
if (!matchOptional(event.type, typeFilter)) continue;
|
|
2125
|
+
if (!matchOptional(event.workItemId, workItemFilter)) continue;
|
|
2126
|
+
if (!textIncludes(event.summary || "", qLower)) continue;
|
|
2127
|
+
hits.push({
|
|
2128
|
+
workspaceName,
|
|
2129
|
+
workspacePath,
|
|
2130
|
+
kind: "event",
|
|
2131
|
+
score: 1,
|
|
2132
|
+
title: `${event.type} \xB7 ${event.stepId}`,
|
|
2133
|
+
snippet: makeSnippet(event.summary || "", qLower),
|
|
2134
|
+
meta: {
|
|
2135
|
+
ts: event.ts,
|
|
2136
|
+
stepId: event.stepId,
|
|
2137
|
+
stepIndex: event.stepIndex,
|
|
2138
|
+
type: event.type,
|
|
2139
|
+
workItemId: event.workItemId
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
if (options.targets === "artifacts" || options.targets === "all") {
|
|
2145
|
+
const artifacts = index.artifacts || [];
|
|
2146
|
+
for (const artifact of artifacts) {
|
|
2147
|
+
const preview = artifact.preview || "";
|
|
2148
|
+
if (!textIncludes(preview, qLower)) continue;
|
|
2149
|
+
hits.push({
|
|
2150
|
+
workspaceName,
|
|
2151
|
+
workspacePath,
|
|
2152
|
+
kind: "artifact",
|
|
2153
|
+
score: 1,
|
|
2154
|
+
title: artifact.path,
|
|
2155
|
+
snippet: makeSnippet(preview, qLower),
|
|
2156
|
+
link: artifact.path,
|
|
2157
|
+
meta: {
|
|
2158
|
+
path: artifact.path,
|
|
2159
|
+
kind: artifact.kind
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
hits.sort((a, b) => {
|
|
2166
|
+
const scoreDiff = scoreHit(b) - scoreHit(a);
|
|
2167
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
2168
|
+
const aTs = typeof a.meta.ts === "string" ? a.meta.ts : "";
|
|
2169
|
+
const bTs = typeof b.meta.ts === "string" ? b.meta.ts : "";
|
|
2170
|
+
return bTs.localeCompare(aTs);
|
|
2171
|
+
});
|
|
2172
|
+
return limit ? hits.slice(0, limit) : hits;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// src/cli/commands/search.ts
|
|
2176
|
+
function collect(value, previous = []) {
|
|
2177
|
+
return previous.concat([value]);
|
|
2178
|
+
}
|
|
2179
|
+
var searchCommand = new Command10("search").description("\u5728 workspace \u7D22\u5F15\u4E0A\u6267\u884C\u641C\u7D22").argument("<query>", "\u641C\u7D22\u5173\u952E\u8BCD").option("--workspace <pathOrName>", "\u6307\u5B9A workspace\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("--step <id>", "\u7B5B\u9009 stepId\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("--type <type>", "\u7B5B\u9009\u4E8B\u4EF6\u7C7B\u578B\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("--work-item <id>", "\u7B5B\u9009 workItemId\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("--limit <n>", "\u9650\u5236\u7ED3\u679C\u6570\u91CF", (v) => parseInt(v, 10)).option("--json", "\u4EE5 JSON \u8F93\u51FA\u7ED3\u679C").action(
|
|
2180
|
+
async (query, options) => {
|
|
2181
|
+
try {
|
|
2182
|
+
const registryItems = await listWorkspaces();
|
|
2183
|
+
const workspaces = options.workspace || [];
|
|
2184
|
+
const registryOnly = workspaces.length === 0;
|
|
2185
|
+
const results = await search(
|
|
2186
|
+
{
|
|
2187
|
+
q: query,
|
|
2188
|
+
workspace: workspaces,
|
|
2189
|
+
stepId: options.step,
|
|
2190
|
+
type: options.type,
|
|
2191
|
+
workItemId: options.workItem,
|
|
2192
|
+
limit: options.limit
|
|
2193
|
+
},
|
|
2194
|
+
{ targets: "all", registryOnly }
|
|
2195
|
+
);
|
|
2196
|
+
if (options.json) {
|
|
2197
|
+
console.log(JSON.stringify(results, null, 2));
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
if (!registryOnly && registryItems.length === 0) {
|
|
2201
|
+
console.log("\u26A0\uFE0F registry \u4E3A\u7A7A\uFF0C\u4F7F\u7528\u547D\u4EE4\u884C\u6307\u5B9A\u7684 workspace");
|
|
2202
|
+
}
|
|
2203
|
+
if (results.length === 0) {
|
|
2204
|
+
console.log("\u672A\u627E\u5230\u5339\u914D\u7ED3\u679C");
|
|
2205
|
+
if (registryItems.length === 0 && registryOnly) {
|
|
2206
|
+
console.log("\u63D0\u793A\uFF1A\u8BF7\u5148\u8FD0\u884C agent-handoff index --add \u6CE8\u518C workspace");
|
|
2207
|
+
}
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
for (const hit of results) {
|
|
2211
|
+
console.log(`${hit.workspaceName} ${hit.kind} ${hit.title}`);
|
|
2212
|
+
if (hit.snippet) {
|
|
2213
|
+
console.log(` ${hit.snippet}`);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
} catch (error) {
|
|
2217
|
+
console.error(`Error: ${error}`);
|
|
2218
|
+
process.exit(1);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
);
|
|
2222
|
+
|
|
2223
|
+
// src/cli/commands/diff.ts
|
|
2224
|
+
import { Command as Command11 } from "commander";
|
|
2225
|
+
import path19 from "path";
|
|
2226
|
+
|
|
2227
|
+
// src/core/diff/diff.ts
|
|
2228
|
+
import fs16 from "fs/promises";
|
|
2229
|
+
import path18 from "path";
|
|
2230
|
+
async function readIndex(workspacePath) {
|
|
2231
|
+
const abs = path18.resolve(workspacePath);
|
|
2232
|
+
const indexPath = path18.join(abs, ".agenthandoff", "index.json");
|
|
2233
|
+
try {
|
|
2234
|
+
const raw = await fs16.readFile(indexPath, "utf-8");
|
|
2235
|
+
return JSON.parse(raw);
|
|
2236
|
+
} catch {
|
|
2237
|
+
throw new Error(`index not found: ${indexPath}. \u8BF7\u5148\u8FD0\u884C agent-handoff index`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
async function readTextFile(filePath) {
|
|
2241
|
+
try {
|
|
2242
|
+
return await fs16.readFile(filePath, "utf-8");
|
|
2243
|
+
} catch {
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
function uniqueSorted(values) {
|
|
2248
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
2249
|
+
}
|
|
2250
|
+
function defaultArtifactPaths(index) {
|
|
2251
|
+
const artifacts = index.artifacts || [];
|
|
2252
|
+
const paths = artifacts.map((a) => a.path);
|
|
2253
|
+
return paths.filter((p) => p === "brief.md" || /^steps\/[^/]+\/output\.md$/.test(p));
|
|
2254
|
+
}
|
|
2255
|
+
function buildLcsOps(a, b) {
|
|
2256
|
+
const n = a.length;
|
|
2257
|
+
const m = b.length;
|
|
2258
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
2259
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
2260
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
2261
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
const ops = [];
|
|
2265
|
+
let i = 0;
|
|
2266
|
+
let j = 0;
|
|
2267
|
+
while (i < n && j < m) {
|
|
2268
|
+
if (a[i] === b[j]) {
|
|
2269
|
+
ops.push({ t: "equal", line: a[i] });
|
|
2270
|
+
i++;
|
|
2271
|
+
j++;
|
|
2272
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
2273
|
+
ops.push({ t: "del", line: a[i] });
|
|
2274
|
+
i++;
|
|
2275
|
+
} else {
|
|
2276
|
+
ops.push({ t: "add", line: b[j] });
|
|
2277
|
+
j++;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
while (i < n) {
|
|
2281
|
+
ops.push({ t: "del", line: a[i] });
|
|
2282
|
+
i++;
|
|
2283
|
+
}
|
|
2284
|
+
while (j < m) {
|
|
2285
|
+
ops.push({ t: "add", line: b[j] });
|
|
2286
|
+
j++;
|
|
2287
|
+
}
|
|
2288
|
+
return ops;
|
|
2289
|
+
}
|
|
2290
|
+
function formatUnifiedDiff(filePath, left, right, contextLines) {
|
|
2291
|
+
const leftLines = (left ?? "").split("\n");
|
|
2292
|
+
const rightLines = (right ?? "").split("\n");
|
|
2293
|
+
const ops = buildLcsOps(leftLines, rightLines);
|
|
2294
|
+
const lines = [];
|
|
2295
|
+
lines.push(`--- a/${filePath}`);
|
|
2296
|
+
lines.push(`+++ b/${filePath}`);
|
|
2297
|
+
let aLine = 1;
|
|
2298
|
+
let bLine = 1;
|
|
2299
|
+
let i = 0;
|
|
2300
|
+
const consumeEqual = (count) => {
|
|
2301
|
+
for (let k = 0; k < count && i < ops.length; k++) {
|
|
2302
|
+
const op = ops[i];
|
|
2303
|
+
if (op.t !== "equal") break;
|
|
2304
|
+
aLine++;
|
|
2305
|
+
bLine++;
|
|
2306
|
+
i++;
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
while (i < ops.length) {
|
|
2310
|
+
while (i < ops.length && ops[i].t === "equal") {
|
|
2311
|
+
i++;
|
|
2312
|
+
aLine++;
|
|
2313
|
+
bLine++;
|
|
2314
|
+
}
|
|
2315
|
+
if (i >= ops.length) break;
|
|
2316
|
+
const hunkStart = Math.max(0, i - contextLines);
|
|
2317
|
+
let hunkEnd = i;
|
|
2318
|
+
let trailingContext = 0;
|
|
2319
|
+
while (hunkEnd < ops.length) {
|
|
2320
|
+
if (ops[hunkEnd].t === "equal") {
|
|
2321
|
+
trailingContext++;
|
|
2322
|
+
if (trailingContext > contextLines) break;
|
|
2323
|
+
} else {
|
|
2324
|
+
trailingContext = 0;
|
|
2325
|
+
}
|
|
2326
|
+
hunkEnd++;
|
|
2327
|
+
}
|
|
2328
|
+
let aStart = aLine;
|
|
2329
|
+
let bStart = bLine;
|
|
2330
|
+
for (let k = i - 1; k >= hunkStart; k--) {
|
|
2331
|
+
const op = ops[k];
|
|
2332
|
+
if (op.t === "equal" || op.t === "del") aStart--;
|
|
2333
|
+
if (op.t === "equal" || op.t === "add") bStart--;
|
|
2334
|
+
}
|
|
2335
|
+
let aLen = 0;
|
|
2336
|
+
let bLen = 0;
|
|
2337
|
+
for (let k = hunkStart; k < hunkEnd; k++) {
|
|
2338
|
+
const op = ops[k];
|
|
2339
|
+
if (op.t === "equal" || op.t === "del") aLen++;
|
|
2340
|
+
if (op.t === "equal" || op.t === "add") bLen++;
|
|
2341
|
+
}
|
|
2342
|
+
lines.push(`@@ -${aStart},${aLen} +${bStart},${bLen} @@`);
|
|
2343
|
+
for (let k = hunkStart; k < hunkEnd; k++) {
|
|
2344
|
+
const op = ops[k];
|
|
2345
|
+
if (op.t === "equal") lines.push(` ${op.line}`);
|
|
2346
|
+
if (op.t === "del") lines.push(`-${op.line}`);
|
|
2347
|
+
if (op.t === "add") lines.push(`+${op.line}`);
|
|
2348
|
+
}
|
|
2349
|
+
i = hunkEnd;
|
|
2350
|
+
consumeEqual(contextLines);
|
|
2351
|
+
}
|
|
2352
|
+
return lines.join("\n");
|
|
2353
|
+
}
|
|
2354
|
+
function diffWorkflow(left, right) {
|
|
2355
|
+
const leftSteps = left.steps || [];
|
|
2356
|
+
const rightSteps = right.steps || [];
|
|
2357
|
+
const leftIds = leftSteps.map((s) => s.id);
|
|
2358
|
+
const rightIds = rightSteps.map((s) => s.id);
|
|
2359
|
+
const leftSet = new Set(leftIds);
|
|
2360
|
+
const rightSet = new Set(rightIds);
|
|
2361
|
+
const added = rightIds.filter((id) => !leftSet.has(id));
|
|
2362
|
+
const removed = leftIds.filter((id) => !rightSet.has(id));
|
|
2363
|
+
let changedSteps = 0;
|
|
2364
|
+
const maxLen = Math.max(leftIds.length, rightIds.length);
|
|
2365
|
+
for (let i = 0; i < maxLen; i++) {
|
|
2366
|
+
if (leftIds[i] !== rightIds[i]) changedSteps++;
|
|
2367
|
+
}
|
|
2368
|
+
const orderChanged = added.length === 0 && removed.length === 0 && leftIds.join("|") !== rightIds.join("|");
|
|
2369
|
+
const changed = added.length > 0 || removed.length > 0 || orderChanged;
|
|
2370
|
+
if (!changed) {
|
|
2371
|
+
return { changed: false, changedSteps: 0 };
|
|
2372
|
+
}
|
|
2373
|
+
const parts = [];
|
|
2374
|
+
if (added.length > 0) parts.push(`added: ${added.join(", ")}`);
|
|
2375
|
+
if (removed.length > 0) parts.push(`removed: ${removed.join(", ")}`);
|
|
2376
|
+
if (orderChanged) parts.push("order: changed");
|
|
2377
|
+
return { changed: true, details: parts.join("\n"), changedSteps };
|
|
2378
|
+
}
|
|
2379
|
+
function diffEvents(left, right) {
|
|
2380
|
+
const leftEvents = left.events || [];
|
|
2381
|
+
const rightEvents = right.events || [];
|
|
2382
|
+
const leftCount = leftEvents.length;
|
|
2383
|
+
const rightCount = rightEvents.length;
|
|
2384
|
+
const added = rightCount > leftCount ? rightCount - leftCount : 0;
|
|
2385
|
+
const latest = rightEvents[rightEvents.length - 1];
|
|
2386
|
+
return {
|
|
2387
|
+
leftCount,
|
|
2388
|
+
rightCount,
|
|
2389
|
+
added,
|
|
2390
|
+
...latest ? { latestRight: { ts: latest.ts, summary: latest.summary } } : {}
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
async function diffWorkspaces(options) {
|
|
2394
|
+
const leftWs = path18.resolve(options.leftWorkspace);
|
|
2395
|
+
const rightWs = path18.resolve(options.rightWorkspace);
|
|
2396
|
+
const leftIndex = await readIndex(leftWs);
|
|
2397
|
+
const rightIndex = await readIndex(rightWs);
|
|
2398
|
+
const contextLines = typeof options.contextLines === "number" && options.contextLines >= 0 ? options.contextLines : 3;
|
|
2399
|
+
const requestedPaths = options.paths && options.paths.length > 0 ? options.paths : void 0;
|
|
2400
|
+
const leftPaths = requestedPaths ? requestedPaths : defaultArtifactPaths(leftIndex);
|
|
2401
|
+
const rightPaths = requestedPaths ? requestedPaths : defaultArtifactPaths(rightIndex);
|
|
2402
|
+
const paths = uniqueSorted([...leftPaths, ...rightPaths]);
|
|
2403
|
+
const results = [];
|
|
2404
|
+
let addedArtifacts = 0;
|
|
2405
|
+
let removedArtifacts = 0;
|
|
2406
|
+
let changedArtifacts = 0;
|
|
2407
|
+
for (const rel of paths) {
|
|
2408
|
+
const leftText = await readTextFile(path18.join(leftWs, rel));
|
|
2409
|
+
const rightText = await readTextFile(path18.join(rightWs, rel));
|
|
2410
|
+
if (leftText === null && rightText === null) {
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
if (leftText === null && rightText !== null) {
|
|
2414
|
+
addedArtifacts++;
|
|
2415
|
+
const diff = formatUnifiedDiff(rel, "", rightText, contextLines);
|
|
2416
|
+
results.push({ path: rel, status: "added", diff });
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
if (leftText !== null && rightText === null) {
|
|
2420
|
+
removedArtifacts++;
|
|
2421
|
+
const diff = formatUnifiedDiff(rel, leftText, "", contextLines);
|
|
2422
|
+
results.push({ path: rel, status: "removed", diff });
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
if (leftText !== rightText) {
|
|
2426
|
+
changedArtifacts++;
|
|
2427
|
+
const diff = formatUnifiedDiff(rel, leftText, rightText, contextLines);
|
|
2428
|
+
results.push({ path: rel, status: "changed", diff });
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
results.push({ path: rel, status: "unchanged" });
|
|
2432
|
+
}
|
|
2433
|
+
const wf = diffWorkflow(leftIndex, rightIndex);
|
|
2434
|
+
const summary = {
|
|
2435
|
+
left: { name: leftIndex.workspaceName, path: leftWs },
|
|
2436
|
+
right: { name: rightIndex.workspaceName, path: rightWs },
|
|
2437
|
+
changedArtifacts,
|
|
2438
|
+
addedArtifacts,
|
|
2439
|
+
removedArtifacts,
|
|
2440
|
+
changedSteps: wf.changedSteps
|
|
2441
|
+
};
|
|
2442
|
+
return {
|
|
2443
|
+
summary,
|
|
2444
|
+
artifacts: results.sort((a, b) => a.path.localeCompare(b.path)),
|
|
2445
|
+
workflow: wf.changed ? { changed: true, details: wf.details } : { changed: false },
|
|
2446
|
+
events: diffEvents(leftIndex, rightIndex)
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
function formatDiffResult(result, format) {
|
|
2450
|
+
const lines = [];
|
|
2451
|
+
const s = result.summary;
|
|
2452
|
+
lines.push(`left: ${s.left.name} ${s.left.path}`);
|
|
2453
|
+
lines.push(`right: ${s.right.name} ${s.right.path}`);
|
|
2454
|
+
lines.push(
|
|
2455
|
+
`artifacts: changed=${s.changedArtifacts} added=${s.addedArtifacts} removed=${s.removedArtifacts} stepsChanged=${s.changedSteps}`
|
|
2456
|
+
);
|
|
2457
|
+
if (result.workflow?.changed) {
|
|
2458
|
+
lines.push("");
|
|
2459
|
+
lines.push("workflow: changed");
|
|
2460
|
+
if (result.workflow.details) lines.push(result.workflow.details);
|
|
2461
|
+
}
|
|
2462
|
+
if (result.events) {
|
|
2463
|
+
lines.push("");
|
|
2464
|
+
lines.push(`events: left=${result.events.leftCount} right=${result.events.rightCount} added=${result.events.added}`);
|
|
2465
|
+
if (result.events.latestRight) {
|
|
2466
|
+
lines.push(`latest: ${result.events.latestRight.ts} ${result.events.latestRight.summary}`);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
for (const a of result.artifacts) {
|
|
2470
|
+
if (a.status === "unchanged") continue;
|
|
2471
|
+
lines.push("");
|
|
2472
|
+
lines.push(`${a.status}: ${a.path}`);
|
|
2473
|
+
if (a.diff) {
|
|
2474
|
+
if (format === "markdown") {
|
|
2475
|
+
lines.push("```diff");
|
|
2476
|
+
lines.push(a.diff);
|
|
2477
|
+
lines.push("```");
|
|
2478
|
+
} else {
|
|
2479
|
+
lines.push(a.diff);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
return lines.join("\n");
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// src/cli/commands/diff.ts
|
|
2487
|
+
function collect2(value, previous = []) {
|
|
2488
|
+
return previous.concat([value]);
|
|
2489
|
+
}
|
|
2490
|
+
async function resolveWorkspaceToken(token) {
|
|
2491
|
+
const resolved = path19.resolve(token);
|
|
2492
|
+
const items = await listWorkspaces().catch(() => []);
|
|
2493
|
+
const byName = items.find((i) => i.name === token);
|
|
2494
|
+
if (byName) return byName.path;
|
|
2495
|
+
return resolved;
|
|
2496
|
+
}
|
|
2497
|
+
var diffCommand = new Command11("diff").description("\u5BF9\u4E24\u4E2A workspace \u8FDB\u884C diff\uFF08\u57FA\u4E8E index.json\uFF09").argument("<left>", "\u5DE6\u4FA7 workspace\uFF08path \u6216 registry name\uFF09").argument("<right>", "\u53F3\u4FA7 workspace\uFF08path \u6216 registry name\uFF09").option("--format <format>", "\u8F93\u51FA\u683C\u5F0F\uFF1Atext|markdown|json", "text").option("--path <path>", "\u4EC5\u5BF9\u6307\u5B9A\u6587\u4EF6\u505A diff\uFF08\u53EF\u91CD\u590D\uFF09", collect2, []).option("--context <n>", "diff context \u884C\u6570", (v) => parseInt(v, 10)).action(
|
|
2498
|
+
async (left, right, options) => {
|
|
2499
|
+
try {
|
|
2500
|
+
const leftPath = await resolveWorkspaceToken(left);
|
|
2501
|
+
const rightPath = await resolveWorkspaceToken(right);
|
|
2502
|
+
const format = options.format === "markdown" || options.format === "json" ? options.format : "text";
|
|
2503
|
+
const paths = options.path && options.path.length > 0 ? options.path : void 0;
|
|
2504
|
+
const result = await diffWorkspaces({
|
|
2505
|
+
leftWorkspace: leftPath,
|
|
2506
|
+
rightWorkspace: rightPath,
|
|
2507
|
+
format,
|
|
2508
|
+
paths,
|
|
2509
|
+
contextLines: options.context
|
|
2510
|
+
});
|
|
2511
|
+
if (format === "json") {
|
|
2512
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
console.log(formatDiffResult(result, format));
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
console.error(`Error: ${error}`);
|
|
2518
|
+
process.exit(1);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
);
|
|
2522
|
+
|
|
2523
|
+
// src/cli/commands/stats.ts
|
|
2524
|
+
import { Command as Command12 } from "commander";
|
|
2525
|
+
import path21 from "path";
|
|
2526
|
+
|
|
2527
|
+
// src/core/stats/stats.ts
|
|
2528
|
+
import fs17 from "fs/promises";
|
|
2529
|
+
import path20 from "path";
|
|
2530
|
+
async function readIndex2(workspacePath) {
|
|
2531
|
+
const abs = path20.resolve(workspacePath);
|
|
2532
|
+
const indexPath = path20.join(abs, ".agenthandoff", "index.json");
|
|
2533
|
+
try {
|
|
2534
|
+
const raw = await fs17.readFile(indexPath, "utf-8");
|
|
2535
|
+
return JSON.parse(raw);
|
|
2536
|
+
} catch {
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
function countEventsByType(events) {
|
|
2541
|
+
const counts = {};
|
|
2542
|
+
for (const ev of events) {
|
|
2543
|
+
const type = ev.type || "unknown";
|
|
2544
|
+
counts[type] = (counts[type] || 0) + 1;
|
|
2545
|
+
}
|
|
2546
|
+
return counts;
|
|
2547
|
+
}
|
|
2548
|
+
function buildDurations(events) {
|
|
2549
|
+
const map = /* @__PURE__ */ new Map();
|
|
2550
|
+
for (const ev of events) {
|
|
2551
|
+
if (!ev.stepId || !ev.type || !ev.ts) continue;
|
|
2552
|
+
const key = ev.stepId;
|
|
2553
|
+
const current = map.get(key) || {};
|
|
2554
|
+
if (ev.type === "step.started") {
|
|
2555
|
+
if (!current.startedAt || ev.ts < current.startedAt) current.startedAt = ev.ts;
|
|
2556
|
+
}
|
|
2557
|
+
if (ev.type === "step.done") {
|
|
2558
|
+
if (!current.doneAt || ev.ts > current.doneAt) current.doneAt = ev.ts;
|
|
2559
|
+
}
|
|
2560
|
+
map.set(key, current);
|
|
2561
|
+
}
|
|
2562
|
+
const items = [];
|
|
2563
|
+
for (const [stepId, v] of map.entries()) {
|
|
2564
|
+
let durationMs;
|
|
2565
|
+
if (v.startedAt && v.doneAt) {
|
|
2566
|
+
const start = new Date(v.startedAt).getTime();
|
|
2567
|
+
const end = new Date(v.doneAt).getTime();
|
|
2568
|
+
if (!Number.isNaN(start) && !Number.isNaN(end) && end >= start) {
|
|
2569
|
+
durationMs = end - start;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
items.push({ stepId, startedAt: v.startedAt, doneAt: v.doneAt, ...durationMs !== void 0 && { durationMs } });
|
|
2573
|
+
}
|
|
2574
|
+
return items.sort((a, b) => a.stepId.localeCompare(b.stepId));
|
|
2575
|
+
}
|
|
2576
|
+
function buildWorkspaceStats(index, mode) {
|
|
2577
|
+
const steps = index.steps || [];
|
|
2578
|
+
const stepsTotal = steps.length;
|
|
2579
|
+
const stepsDone = steps.filter((s) => Boolean(s.outputExists)).length;
|
|
2580
|
+
const events = index.events || [];
|
|
2581
|
+
const eventsTotal = events.length;
|
|
2582
|
+
const eventsByType = countEventsByType(events);
|
|
2583
|
+
const automationSessions = events.filter((e) => e.type === "automation.session").length;
|
|
2584
|
+
const stats = {
|
|
2585
|
+
workspaceName: index.workspaceName,
|
|
2586
|
+
workspacePath: index.workspacePath,
|
|
2587
|
+
indexedAt: index.indexedAt,
|
|
2588
|
+
stepsTotal,
|
|
2589
|
+
stepsDone,
|
|
2590
|
+
eventsTotal,
|
|
2591
|
+
eventsByType,
|
|
2592
|
+
automation: {
|
|
2593
|
+
sessions: automationSessions,
|
|
2594
|
+
summary: {}
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
if (mode === "full") {
|
|
2598
|
+
stats.durations = buildDurations(events);
|
|
2599
|
+
}
|
|
2600
|
+
return stats;
|
|
2601
|
+
}
|
|
2602
|
+
async function buildStats(query) {
|
|
2603
|
+
const workspaces = query.workspaces || [];
|
|
2604
|
+
const results = [];
|
|
2605
|
+
for (const ws of workspaces) {
|
|
2606
|
+
const index = await readIndex2(ws);
|
|
2607
|
+
if (!index) continue;
|
|
2608
|
+
results.push(buildWorkspaceStats(index, query.mode));
|
|
2609
|
+
}
|
|
2610
|
+
return {
|
|
2611
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2612
|
+
workspaces: results
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// src/cli/commands/stats.ts
|
|
2617
|
+
function isValidFormat(value) {
|
|
2618
|
+
return value === "json" || value === "markdown";
|
|
2619
|
+
}
|
|
2620
|
+
async function resolveWorkspaceToken2(token) {
|
|
2621
|
+
const resolved = path21.resolve(token);
|
|
2622
|
+
const items = await listWorkspaces().catch(() => []);
|
|
2623
|
+
const byName = items.find((i) => i.name === token);
|
|
2624
|
+
return byName ? byName.path : resolved;
|
|
2625
|
+
}
|
|
2626
|
+
function formatMarkdown(result) {
|
|
2627
|
+
const lines = [];
|
|
2628
|
+
lines.push("| Workspace | Steps | Events | Automation |");
|
|
2629
|
+
lines.push("|---|---|---|---|");
|
|
2630
|
+
for (const ws of result.workspaces) {
|
|
2631
|
+
const steps = `${ws.stepsDone}/${ws.stepsTotal}`;
|
|
2632
|
+
const events = `${ws.eventsTotal}`;
|
|
2633
|
+
const automation = `${ws.automation?.sessions ?? 0}`;
|
|
2634
|
+
lines.push(`| ${ws.workspaceName} | ${steps} | ${events} | ${automation} |`);
|
|
2635
|
+
}
|
|
2636
|
+
if (result.workspaces.length === 0) {
|
|
2637
|
+
lines.push("| - | - | - | - |");
|
|
2638
|
+
}
|
|
2639
|
+
return lines.join("\n");
|
|
2640
|
+
}
|
|
2641
|
+
var statsCommand = new Command12("stats").description("\u8F93\u51FA workspace \u7EDF\u8BA1\u4FE1\u606F").argument("[workspaces...]", "workspace \u8DEF\u5F84\u6216 registry name").option("--registry", "\u7EDF\u8BA1 registry \u4E2D\u6240\u6709 workspace").option("--mode <mode>", "summary|full", "summary").option("--format <format>", "json|markdown", "markdown").action(
|
|
2642
|
+
async (workspaces, options) => {
|
|
2643
|
+
try {
|
|
2644
|
+
const mode = options.mode === "full" ? "full" : "summary";
|
|
2645
|
+
const format = isValidFormat(options.format) ? options.format : "markdown";
|
|
2646
|
+
let targets = [];
|
|
2647
|
+
if (options.registry) {
|
|
2648
|
+
const items = await listWorkspaces();
|
|
2649
|
+
targets = items.map((i) => i.path);
|
|
2650
|
+
} else if (workspaces.length > 0) {
|
|
2651
|
+
targets = await Promise.all(workspaces.map(resolveWorkspaceToken2));
|
|
2652
|
+
}
|
|
2653
|
+
if (targets.length === 0) {
|
|
2654
|
+
console.log("\u672A\u627E\u5230\u53EF\u7EDF\u8BA1\u7684 workspace");
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
const result = await buildStats({ workspaces: targets, mode });
|
|
2658
|
+
if (format === "json") {
|
|
2659
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
console.log(formatMarkdown(result));
|
|
2663
|
+
} catch (error) {
|
|
2664
|
+
console.error(`Error: ${error}`);
|
|
2665
|
+
process.exit(1);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
);
|
|
2669
|
+
|
|
1248
2670
|
// src/index.ts
|
|
1249
2671
|
var require2 = createRequire(import.meta.url);
|
|
1250
2672
|
var { version } = require2("../package.json");
|
|
1251
|
-
var program = new
|
|
2673
|
+
var program = new Command13();
|
|
1252
2674
|
program.name("agent-handoff").description("\u8F7B\u91CF\u7EA7\u591A Agent \u534F\u4F5C\u63A5\u529B\u5DE5\u5177").version(version);
|
|
1253
2675
|
program.addCommand(initCommand);
|
|
1254
2676
|
program.addCommand(statusCommand);
|
|
@@ -1257,5 +2679,10 @@ program.addCommand(validateCommand);
|
|
|
1257
2679
|
program.addCommand(advanceCommand);
|
|
1258
2680
|
program.addCommand(configCommand);
|
|
1259
2681
|
program.addCommand(reportCommand);
|
|
2682
|
+
program.addCommand(exportCommand);
|
|
2683
|
+
program.addCommand(indexCommand);
|
|
2684
|
+
program.addCommand(searchCommand);
|
|
2685
|
+
program.addCommand(diffCommand);
|
|
2686
|
+
program.addCommand(statsCommand);
|
|
1260
2687
|
program.parse();
|
|
1261
2688
|
//# sourceMappingURL=index.js.map
|