chainlesschain 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{Analytics-C1AnPdMx.js → Analytics-DgypYeUB.js} +2 -2
- package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +1 -0
- package/src/assets/web-panel/assets/{Backup-D31iZX3l.js → Backup-Ba9UybpT.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DiXJ3TuK.js → Chat-BwXskT21.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-B8ZDdRm4.js → Cowork-UmOe7qvE.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-DBt1ueXh.js → Cron-JHS-rc-4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-jt6XPIjB.js → Dashboard-B95cMCO7.js} +1 -1
- package/src/assets/web-panel/assets/{Git-hwQ1oZHj.js → Git-CSYO0_zk.js} +2 -2
- package/src/assets/web-panel/assets/{Logs-4D9p6PRM.js → Logs-Hxw_K0km.js} +2 -2
- package/src/assets/web-panel/assets/{McpTools-CyAUjbbs.js → McpTools-DIE75TrB.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-BMqOR7S-.js → Memory-C4KVnLlp.js} +2 -2
- package/src/assets/web-panel/assets/{Notes-Cmas8i4E.js → Notes-DuzrHMAk.js} +2 -2
- package/src/assets/web-panel/assets/{Organization-DnSa58Tl.js → Organization-DTq6uF82.js} +4 -4
- package/src/assets/web-panel/assets/{P2P-BxksIBWs.js → P2P-C0hjlhsR.js} +2 -2
- package/src/assets/web-panel/assets/{Permissions-Bq5Qn2s3.js → Permissions-Ec0NH-xC.js} +4 -4
- package/src/assets/web-panel/assets/{Projects-B7EM0uPg.js → Projects-U8D0asCS.js} +2 -2
- package/src/assets/web-panel/assets/{Providers-DAwgG5KV.js → Providers-BngtTLvJ.js} +2 -2
- package/src/assets/web-panel/assets/{RssFeed-HSZoRXvS.js → RssFeed-B9NbwCKM.js} +3 -3
- package/src/assets/web-panel/assets/{Security-Cz17qBny.js → Security-BL5Rkr1T.js} +3 -3
- package/src/assets/web-panel/assets/{Services-D2EsLq-v.js → Services-D4MJzLld.js} +2 -2
- package/src/assets/web-panel/assets/{Skills-C9v-f3vZ.js → Skills-CQTOMDwF.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-yMEcU0n7.js → Tasks-DepbJMnL.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-l7SvlKuB.js → Templates-C24PVZPu.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BHWhLWn9.js → Wallet-PQoSpN_P.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-kWhFYaUK.js → WebAuthn-BcuyQ4Lr.js} +4 -4
- package/src/assets/web-panel/assets/WorkflowEditor-C-SvXbHW.js +1 -0
- package/src/assets/web-panel/assets/WorkflowEditor-D5bX6woe.css +1 -0
- package/src/assets/web-panel/assets/{antd-D6h4fDFf.js → antd-DEjZPGMj.js} +82 -82
- package/src/assets/web-panel/assets/index-CwvzTTw_.js +2 -0
- package/src/assets/web-panel/assets/{markdown-BZsB-Dsv.js → markdown-CusdXFxb.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/cowork.js +213 -41
- package/src/gateways/ws/action-protocol.js +140 -0
- package/src/gateways/ws/message-dispatcher.js +5 -0
- package/src/gateways/ws/ws-server.js +21 -0
- package/src/lib/cowork-evomap-adapter.js +121 -0
- package/src/lib/cowork-observe-html.js +108 -0
- package/src/lib/cowork-observe.js +160 -0
- package/src/lib/cowork-share.js +114 -10
- package/src/lib/provider-options.js +133 -0
- package/src/lib/skill-loader.js +65 -0
- package/src/lib/sub-agent-context.js +16 -4
- package/src/lib/sub-agent-profiles.js +164 -0
- package/src/lib/todo-manager.js +108 -0
- package/src/lib/turn-context.js +95 -0
- package/src/lib/web-fetch.js +224 -0
- package/src/repl/agent-repl.js +4 -0
- package/src/runtime/agent-core.js +135 -3
- package/src/runtime/coding-agent-contract-shared.cjs +131 -0
- package/src/runtime/coding-agent-policy.cjs +30 -0
- package/src/assets/web-panel/assets/AppLayout-BnvARObz.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-cxfKLu-m.css +0 -1
- package/src/assets/web-panel/assets/index-C1SPm_5l.js +0 -2
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cowork ↔ EvoMap adapter — publish Cowork templates as EvoMap "genes" and
|
|
3
|
+
* pull them back for local install. Thin wrapper over `evomap-client.js`
|
|
4
|
+
* that fixes `kind = "cowork-template"` and carries N4 signatures through.
|
|
5
|
+
*
|
|
6
|
+
* Pure glue: all I/O is delegated to EvoMapClient (network) and the
|
|
7
|
+
* marketplace/share modules (disk). Injectable via `_deps.createClient`.
|
|
8
|
+
*
|
|
9
|
+
* @module cowork-evomap-adapter
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { EvoMapClient } from "./evomap-client.js";
|
|
13
|
+
import {
|
|
14
|
+
toShareableTemplate,
|
|
15
|
+
saveUserTemplate,
|
|
16
|
+
} from "./cowork-template-marketplace.js";
|
|
17
|
+
import { buildPacket } from "./cowork-share.js";
|
|
18
|
+
|
|
19
|
+
export const _deps = {
|
|
20
|
+
createClient: (opts) => new EvoMapClient(opts),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const KIND = "cowork-template";
|
|
24
|
+
|
|
25
|
+
function _wrapGene(template, signer) {
|
|
26
|
+
const payload = toShareableTemplate(template);
|
|
27
|
+
// Reuse share-packet builder so signed genes land on the hub with the same
|
|
28
|
+
// canonical shape as file-based packets (checksum + optional signature).
|
|
29
|
+
const packet = buildPacket({
|
|
30
|
+
kind: "template",
|
|
31
|
+
payload,
|
|
32
|
+
author: signer?.did || "anonymous",
|
|
33
|
+
cliVersion: undefined,
|
|
34
|
+
signer: signer || undefined,
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
id: payload.id,
|
|
38
|
+
name: payload.name || payload.id,
|
|
39
|
+
description: payload.description || "",
|
|
40
|
+
kind: KIND,
|
|
41
|
+
version: payload.version || "1.0.0",
|
|
42
|
+
packet,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Publish a template to a hub. Requires API key on the client.
|
|
48
|
+
* @returns {Promise<object>} hub response (typically { id, ... })
|
|
49
|
+
*/
|
|
50
|
+
export async function publishTemplateToHub(
|
|
51
|
+
template,
|
|
52
|
+
{ hubUrl, apiKey, signer } = {},
|
|
53
|
+
) {
|
|
54
|
+
if (!template || !template.id) {
|
|
55
|
+
throw new Error("template.id required");
|
|
56
|
+
}
|
|
57
|
+
const client = _deps.createClient({ hubUrl, apiKey });
|
|
58
|
+
const gene = _wrapGene(template, signer);
|
|
59
|
+
return client.publish(gene);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Search templates on a hub. Degrades to [] on network error unless
|
|
64
|
+
* `strict: true` is passed.
|
|
65
|
+
* @returns {Promise<Array>} annotated with `_hubMeta`
|
|
66
|
+
*/
|
|
67
|
+
export async function searchTemplatesInHub(
|
|
68
|
+
query,
|
|
69
|
+
{ hubUrl, limit = 20, strict = false } = {},
|
|
70
|
+
) {
|
|
71
|
+
const client = _deps.createClient({ hubUrl });
|
|
72
|
+
try {
|
|
73
|
+
const results = await client.search(query || "", {
|
|
74
|
+
category: KIND,
|
|
75
|
+
limit,
|
|
76
|
+
});
|
|
77
|
+
return (results || []).map((r) => ({
|
|
78
|
+
...r,
|
|
79
|
+
_hubMeta: {
|
|
80
|
+
hubUrl: client.hubUrl,
|
|
81
|
+
downloads: r.downloads || 0,
|
|
82
|
+
rating: r.rating || null,
|
|
83
|
+
},
|
|
84
|
+
}));
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (strict) throw err;
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fetch a gene by id and install its template into the local marketplace.
|
|
93
|
+
* Returns the saved template object.
|
|
94
|
+
*/
|
|
95
|
+
export async function installTemplateFromHub(
|
|
96
|
+
cwd,
|
|
97
|
+
geneId,
|
|
98
|
+
{ hubUrl, requireSigned = false, trustedDids = null } = {},
|
|
99
|
+
) {
|
|
100
|
+
const client = _deps.createClient({ hubUrl });
|
|
101
|
+
const data = await client.download(geneId);
|
|
102
|
+
// Hub may return { gene: { packet } } or { packet } directly
|
|
103
|
+
const gene = data?.gene || data;
|
|
104
|
+
const packet = gene?.packet || data?.packet;
|
|
105
|
+
if (!packet || packet.kind !== "template" || !packet.payload) {
|
|
106
|
+
throw new Error("Hub response missing template packet");
|
|
107
|
+
}
|
|
108
|
+
if (requireSigned && !packet.signature) {
|
|
109
|
+
throw new Error("Gene is not signed and --require-signed was set");
|
|
110
|
+
}
|
|
111
|
+
if (
|
|
112
|
+
Array.isArray(trustedDids) &&
|
|
113
|
+
trustedDids.length > 0 &&
|
|
114
|
+
(!packet.signature || !trustedDids.includes(packet.signature.did))
|
|
115
|
+
) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Gene signer not in trusted list${packet.signature ? ` (${packet.signature.did})` : ""}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return saveUserTemplate(cwd, packet.payload);
|
|
121
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cowork Observe HTML — builds a static single-page dashboard from an
|
|
3
|
+
* aggregate snapshot. Pure function; no I/O.
|
|
4
|
+
*
|
|
5
|
+
* @module cowork-observe-html
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function escapeHtml(s) {
|
|
9
|
+
return String(s)
|
|
10
|
+
.replace(/&/g, "&")
|
|
11
|
+
.replace(/</g, "<")
|
|
12
|
+
.replace(/>/g, ">")
|
|
13
|
+
.replace(/"/g, """)
|
|
14
|
+
.replace(/'/g, "'");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function escapeJsonForScript(obj) {
|
|
18
|
+
return JSON.stringify(obj)
|
|
19
|
+
.replace(/</g, "\\u003c")
|
|
20
|
+
.replace(/>/g, "\\u003e")
|
|
21
|
+
.replace(/&/g, "\\u0026");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a self-contained HTML dashboard from aggregate output.
|
|
26
|
+
* @param {object} data - result of `aggregate()`
|
|
27
|
+
* @returns {string} full HTML document
|
|
28
|
+
*/
|
|
29
|
+
export function buildHtml(data) {
|
|
30
|
+
const safeJson = escapeJsonForScript(data || {});
|
|
31
|
+
const win = data?.window || {};
|
|
32
|
+
const t = data?.tasks || {};
|
|
33
|
+
const templates = data?.templates || [];
|
|
34
|
+
const failures = data?.failures || [];
|
|
35
|
+
const next = data?.schedules?.nextTriggers || [];
|
|
36
|
+
|
|
37
|
+
const templateRows = templates
|
|
38
|
+
.slice(0, 10)
|
|
39
|
+
.map(
|
|
40
|
+
(row) =>
|
|
41
|
+
`<tr><td>${escapeHtml(row.templateName || row.templateId)}</td>` +
|
|
42
|
+
`<td>${row.runs}</td><td>${Math.round((row.successRate || 0) * 100)}%</td>` +
|
|
43
|
+
`<td>${row.avgTokens || 0}</td></tr>`,
|
|
44
|
+
)
|
|
45
|
+
.join("");
|
|
46
|
+
|
|
47
|
+
const failureRows = failures
|
|
48
|
+
.slice(0, 10)
|
|
49
|
+
.map(
|
|
50
|
+
(row) =>
|
|
51
|
+
`<tr><td>${escapeHtml(row.templateName || row.templateId)}</td>` +
|
|
52
|
+
`<td>${row.failureCount}</td>` +
|
|
53
|
+
`<td>${escapeHtml((row.commonSummaries?.[0]?.summary || "—").slice(0, 80))}</td></tr>`,
|
|
54
|
+
)
|
|
55
|
+
.join("");
|
|
56
|
+
|
|
57
|
+
const nextRows = next
|
|
58
|
+
.map(
|
|
59
|
+
(n) =>
|
|
60
|
+
`<tr><td>${escapeHtml(n.at)}</td><td>${escapeHtml(n.cron)}</td><td>${escapeHtml(n.scheduleId || "—")}</td></tr>`,
|
|
61
|
+
)
|
|
62
|
+
.join("");
|
|
63
|
+
|
|
64
|
+
return `<!DOCTYPE html>
|
|
65
|
+
<html lang="zh-CN">
|
|
66
|
+
<head>
|
|
67
|
+
<meta charset="UTF-8">
|
|
68
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
69
|
+
<title>Cowork Observe — ChainlessChain</title>
|
|
70
|
+
<script>window.__COWORK_OBSERVE__ = ${safeJson};</script>
|
|
71
|
+
<style>
|
|
72
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; padding: 20px; background: #0e1116; color: #e6edf3; }
|
|
73
|
+
h1 { margin: 0 0 8px; font-size: 22px; }
|
|
74
|
+
h2 { margin: 24px 0 8px; font-size: 16px; color: #9ec5ff; }
|
|
75
|
+
.meta { color: #7d8590; font-size: 13px; }
|
|
76
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin: 16px 0; }
|
|
77
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 14px; }
|
|
78
|
+
.card .num { font-size: 24px; font-weight: 600; color: #58a6ff; }
|
|
79
|
+
.card .lbl { color: #7d8590; font-size: 12px; margin-top: 4px; }
|
|
80
|
+
table { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 6px; overflow: hidden; }
|
|
81
|
+
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #30363d; font-size: 13px; }
|
|
82
|
+
th { background: #1f242c; color: #9ec5ff; font-weight: 500; }
|
|
83
|
+
tr:last-child td { border-bottom: none; }
|
|
84
|
+
.empty { color: #7d8590; font-style: italic; padding: 12px 0; }
|
|
85
|
+
</style>
|
|
86
|
+
</head>
|
|
87
|
+
<body>
|
|
88
|
+
<h1>Cowork Observe</h1>
|
|
89
|
+
<div class="meta">Window: ${escapeHtml(win.days || 7)} days · ${escapeHtml(win.from || "")} → ${escapeHtml(win.to || "")}</div>
|
|
90
|
+
<div class="cards">
|
|
91
|
+
<div class="card"><div class="num">${t.total || 0}</div><div class="lbl">Tasks</div></div>
|
|
92
|
+
<div class="card"><div class="num">${Math.round((t.successRate || 0) * 100)}%</div><div class="lbl">Success rate</div></div>
|
|
93
|
+
<div class="card"><div class="num">${t.failed || 0}</div><div class="lbl">Failed</div></div>
|
|
94
|
+
<div class="card"><div class="num">${t.avgTokens || 0}</div><div class="lbl">Avg tokens</div></div>
|
|
95
|
+
<div class="card"><div class="num">${data?.schedules?.active || 0}</div><div class="lbl">Active schedules</div></div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<h2>Templates (top 10 by runs)</h2>
|
|
99
|
+
${templates.length ? `<table><thead><tr><th>Template</th><th>Runs</th><th>Success</th><th>Avg tokens</th></tr></thead><tbody>${templateRows}</tbody></table>` : `<div class="empty">No template runs in window.</div>`}
|
|
100
|
+
|
|
101
|
+
<h2>Failures (top 10)</h2>
|
|
102
|
+
${failures.length ? `<table><thead><tr><th>Template</th><th>Count</th><th>Common summary</th></tr></thead><tbody>${failureRows}</tbody></table>` : `<div class="empty">No failures in window — nice.</div>`}
|
|
103
|
+
|
|
104
|
+
<h2>Next scheduled triggers</h2>
|
|
105
|
+
${next.length ? `<table><thead><tr><th>At</th><th>Cron</th><th>Schedule</th></tr></thead><tbody>${nextRows}</tbody></table>` : `<div class="empty">No upcoming triggers.</div>`}
|
|
106
|
+
</body>
|
|
107
|
+
</html>`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cowork Observe — aggregate view over Cowork task/workflow/schedule history.
|
|
3
|
+
*
|
|
4
|
+
* Produces a single snapshot combining:
|
|
5
|
+
* - task history (from F9 learning layer)
|
|
6
|
+
* - workflow run history (from F6)
|
|
7
|
+
* - active schedules + next fire times (from F5 cron)
|
|
8
|
+
*
|
|
9
|
+
* Pure + `_deps`-injected for testability. Reads files, never writes.
|
|
10
|
+
*
|
|
11
|
+
* @module cowork-observe
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
loadHistory,
|
|
18
|
+
computeTemplateStats,
|
|
19
|
+
summarizeFailures,
|
|
20
|
+
} from "./cowork-learning.js";
|
|
21
|
+
import { loadSchedules, parseCron } from "./cowork-cron.js";
|
|
22
|
+
|
|
23
|
+
export const _deps = {
|
|
24
|
+
existsSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
now: () => new Date(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function _parseTs(s) {
|
|
32
|
+
if (!s) return 0;
|
|
33
|
+
const t = Date.parse(s);
|
|
34
|
+
return Number.isFinite(t) ? t : 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _loadWorkflowHistory(cwd, cutoffMs) {
|
|
38
|
+
const file = join(cwd, ".chainlesschain", "cowork", "workflow-history.jsonl");
|
|
39
|
+
if (!_deps.existsSync(file)) return [];
|
|
40
|
+
const raw = _deps.readFileSync(file, "utf-8");
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const line of raw.split("\n")) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed) continue;
|
|
45
|
+
try {
|
|
46
|
+
const rec = JSON.parse(trimmed);
|
|
47
|
+
if (_parseTs(rec.startedAt || rec.timestamp) >= cutoffMs) out.push(rec);
|
|
48
|
+
} catch (_e) {
|
|
49
|
+
// skip malformed
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Most recent first, top 10
|
|
53
|
+
out.sort(
|
|
54
|
+
(a, b) =>
|
|
55
|
+
_parseTs(b.startedAt || b.timestamp) -
|
|
56
|
+
_parseTs(a.startedAt || a.timestamp),
|
|
57
|
+
);
|
|
58
|
+
return out.slice(0, 10);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Compute the next N fire times across all enabled schedules.
|
|
63
|
+
* Probes minute-by-minute from `from` for up to `maxMinutesAhead` (default 7d).
|
|
64
|
+
*
|
|
65
|
+
* Exported for testability.
|
|
66
|
+
*/
|
|
67
|
+
export function _computeNextTriggers(schedules, from, limit = 5) {
|
|
68
|
+
const enabled = (schedules || []).filter((s) => s && s.enabled !== false);
|
|
69
|
+
if (enabled.length === 0) return [];
|
|
70
|
+
const matchers = [];
|
|
71
|
+
for (const s of enabled) {
|
|
72
|
+
try {
|
|
73
|
+
matchers.push({ id: s.id, cron: s.cron, match: parseCron(s.cron) });
|
|
74
|
+
} catch (_e) {
|
|
75
|
+
// skip invalid cron
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (matchers.length === 0) return [];
|
|
79
|
+
|
|
80
|
+
const out = [];
|
|
81
|
+
const MAX_MINUTES = 60 * 24 * 7; // 1 week window is enough for typical cadences
|
|
82
|
+
const start = new Date(from);
|
|
83
|
+
start.setSeconds(0, 0);
|
|
84
|
+
start.setMinutes(start.getMinutes() + 1); // first candidate = next whole minute
|
|
85
|
+
const cursor = new Date(start);
|
|
86
|
+
for (let i = 0; i < MAX_MINUTES && out.length < limit; i++) {
|
|
87
|
+
for (const m of matchers) {
|
|
88
|
+
if (m.match(cursor)) {
|
|
89
|
+
out.push({
|
|
90
|
+
scheduleId: m.id,
|
|
91
|
+
cron: m.cron,
|
|
92
|
+
at: new Date(cursor).toISOString(),
|
|
93
|
+
});
|
|
94
|
+
if (out.length >= limit) break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Main aggregate ──────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Aggregate all Cowork state for the given window.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} cwd
|
|
108
|
+
* @param {object} [options]
|
|
109
|
+
* @param {number} [options.windowDays=7]
|
|
110
|
+
* @returns {{
|
|
111
|
+
* window: { days: number, from: string, to: string },
|
|
112
|
+
* tasks: { total, completed, failed, successRate, avgTokens },
|
|
113
|
+
* templates: Array, failures: Array, workflows: Array,
|
|
114
|
+
* schedules: { active: number, nextTriggers: Array },
|
|
115
|
+
* }}
|
|
116
|
+
*/
|
|
117
|
+
export function aggregate(cwd, { windowDays = 7 } = {}) {
|
|
118
|
+
const now = _deps.now();
|
|
119
|
+
const cutoff = now.getTime() - windowDays * 86400_000;
|
|
120
|
+
const allHistory = loadHistory(cwd);
|
|
121
|
+
const history = allHistory.filter((r) => _parseTs(r.timestamp) >= cutoff);
|
|
122
|
+
|
|
123
|
+
const total = history.length;
|
|
124
|
+
const completed = history.filter((r) => r.status === "completed").length;
|
|
125
|
+
const failed = history.filter((r) => r.status === "failed").length;
|
|
126
|
+
let tokenSum = 0;
|
|
127
|
+
let tokenCount = 0;
|
|
128
|
+
for (const r of history) {
|
|
129
|
+
const t = Number(r.result?.tokenCount || 0);
|
|
130
|
+
if (t > 0) {
|
|
131
|
+
tokenSum += t;
|
|
132
|
+
tokenCount += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const schedules = loadSchedules(cwd);
|
|
137
|
+
const activeSchedules = schedules.filter((s) => s && s.enabled !== false);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
window: {
|
|
141
|
+
days: windowDays,
|
|
142
|
+
from: new Date(cutoff).toISOString(),
|
|
143
|
+
to: now.toISOString(),
|
|
144
|
+
},
|
|
145
|
+
tasks: {
|
|
146
|
+
total,
|
|
147
|
+
completed,
|
|
148
|
+
failed,
|
|
149
|
+
successRate: total > 0 ? +(completed / total).toFixed(3) : 0,
|
|
150
|
+
avgTokens: tokenCount > 0 ? Math.round(tokenSum / tokenCount) : 0,
|
|
151
|
+
},
|
|
152
|
+
templates: computeTemplateStats(history),
|
|
153
|
+
failures: summarizeFailures(history),
|
|
154
|
+
workflows: _loadWorkflowHistory(cwd, cutoff),
|
|
155
|
+
schedules: {
|
|
156
|
+
active: activeSchedules.length,
|
|
157
|
+
nextTriggers: _computeNextTriggers(schedules, now, 5),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
package/src/lib/cowork-share.js
CHANGED
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
|
-
import { createHash } from "node:crypto";
|
|
20
|
+
import crypto, { createHash } from "node:crypto";
|
|
21
21
|
import {
|
|
22
22
|
toShareableTemplate,
|
|
23
23
|
saveUserTemplate,
|
|
24
24
|
} from "./cowork-template-marketplace.js";
|
|
25
|
+
import { generateDID } from "./did-manager.js";
|
|
25
26
|
|
|
26
27
|
export const _deps = {
|
|
27
28
|
existsSync,
|
|
@@ -31,6 +32,8 @@ export const _deps = {
|
|
|
31
32
|
now: () => new Date().toISOString(),
|
|
32
33
|
};
|
|
33
34
|
|
|
35
|
+
const SUPPORTED_SIG_ALG = "Ed25519";
|
|
36
|
+
|
|
34
37
|
const PACKET_VERSION = 1;
|
|
35
38
|
const PACKET_KINDS = ["template", "result"];
|
|
36
39
|
|
|
@@ -62,7 +65,13 @@ function sha256Hex(s) {
|
|
|
62
65
|
* filled with createdAt/cliVersion defaults; checksum is computed over the
|
|
63
66
|
* canonical form of `{ kind, version, payload, meta }`.
|
|
64
67
|
*/
|
|
65
|
-
export function buildPacket({
|
|
68
|
+
export function buildPacket({
|
|
69
|
+
kind,
|
|
70
|
+
payload,
|
|
71
|
+
author,
|
|
72
|
+
cliVersion,
|
|
73
|
+
signer,
|
|
74
|
+
} = {}) {
|
|
66
75
|
if (!PACKET_KINDS.includes(kind)) {
|
|
67
76
|
throw new Error(`kind must be one of ${PACKET_KINDS.join(", ")}`);
|
|
68
77
|
}
|
|
@@ -76,7 +85,74 @@ export function buildPacket({ kind, payload, author, cliVersion } = {}) {
|
|
|
76
85
|
};
|
|
77
86
|
const body = { kind, version: PACKET_VERSION, payload, meta };
|
|
78
87
|
const checksum = sha256Hex(canonicalize(body));
|
|
79
|
-
|
|
88
|
+
const packet = { ...body, checksum };
|
|
89
|
+
if (signer) {
|
|
90
|
+
packet.signature = _signBody(body, signer);
|
|
91
|
+
}
|
|
92
|
+
return packet;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Signatures (Ed25519) ────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function _signBody(body, signer) {
|
|
98
|
+
if (!signer.did || typeof signer.did !== "string") {
|
|
99
|
+
throw new Error("signer.did required");
|
|
100
|
+
}
|
|
101
|
+
if (!signer.privateKey || !signer.publicKey) {
|
|
102
|
+
throw new Error("signer.privateKey and signer.publicKey required (hex)");
|
|
103
|
+
}
|
|
104
|
+
if (signer.alg && signer.alg !== SUPPORTED_SIG_ALG) {
|
|
105
|
+
throw new Error(`unsupported signature alg: ${signer.alg}`);
|
|
106
|
+
}
|
|
107
|
+
// Verify did matches publicKey (prevents accidental DID spoofing in signer)
|
|
108
|
+
const expectedDid = generateDID(signer.publicKey);
|
|
109
|
+
if (signer.did !== expectedDid) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`signer.did does not match publicKey (expected ${expectedDid})`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const privKey = crypto.createPrivateKey({
|
|
115
|
+
key: Buffer.from(signer.privateKey, "hex"),
|
|
116
|
+
format: "der",
|
|
117
|
+
type: "pkcs8",
|
|
118
|
+
});
|
|
119
|
+
const bytes = Buffer.from(canonicalize(body), "utf-8");
|
|
120
|
+
const sig = crypto.sign(null, bytes, privKey);
|
|
121
|
+
return {
|
|
122
|
+
alg: SUPPORTED_SIG_ALG,
|
|
123
|
+
did: signer.did,
|
|
124
|
+
publicKey: signer.publicKey,
|
|
125
|
+
sig: sig.toString("base64url").replace(/=+$/, ""),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _verifySignature(body, signature) {
|
|
130
|
+
if (!signature || typeof signature !== "object") {
|
|
131
|
+
return { valid: false, error: "signature missing" };
|
|
132
|
+
}
|
|
133
|
+
if (signature.alg !== SUPPORTED_SIG_ALG) {
|
|
134
|
+
return { valid: false, error: `unsupported alg '${signature.alg}'` };
|
|
135
|
+
}
|
|
136
|
+
if (!signature.did || !signature.publicKey || !signature.sig) {
|
|
137
|
+
return { valid: false, error: "signature fields incomplete" };
|
|
138
|
+
}
|
|
139
|
+
const expectedDid = generateDID(signature.publicKey);
|
|
140
|
+
if (signature.did !== expectedDid) {
|
|
141
|
+
return { valid: false, error: "did does not match embedded publicKey" };
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const pubKey = crypto.createPublicKey({
|
|
145
|
+
key: Buffer.from(signature.publicKey, "hex"),
|
|
146
|
+
format: "der",
|
|
147
|
+
type: "spki",
|
|
148
|
+
});
|
|
149
|
+
const bytes = Buffer.from(canonicalize(body), "utf-8");
|
|
150
|
+
const sigBytes = Buffer.from(signature.sig, "base64url");
|
|
151
|
+
const ok = crypto.verify(null, bytes, pubKey, sigBytes);
|
|
152
|
+
return { valid: ok, error: ok ? null : "signature invalid" };
|
|
153
|
+
} catch (err) {
|
|
154
|
+
return { valid: false, error: `signature verify error: ${err.message}` };
|
|
155
|
+
}
|
|
80
156
|
}
|
|
81
157
|
|
|
82
158
|
/**
|
|
@@ -103,12 +179,18 @@ export function verifyPacket(packet) {
|
|
|
103
179
|
if (!packet.checksum) errors.push("checksum missing");
|
|
104
180
|
if (errors.length > 0) return { valid: false, errors };
|
|
105
181
|
|
|
106
|
-
const { checksum, ...body } = packet;
|
|
182
|
+
const { checksum, signature, ...body } = packet;
|
|
107
183
|
const expected = sha256Hex(canonicalize(body));
|
|
108
184
|
if (expected !== checksum) {
|
|
109
185
|
errors.push("checksum mismatch (packet may be corrupted or tampered with)");
|
|
110
186
|
}
|
|
111
|
-
|
|
187
|
+
if (signature !== undefined) {
|
|
188
|
+
const sigRes = _verifySignature(body, signature);
|
|
189
|
+
if (!sigRes.valid) {
|
|
190
|
+
errors.push(sigRes.error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { valid: errors.length === 0, errors, signed: !!signature };
|
|
112
194
|
}
|
|
113
195
|
|
|
114
196
|
// ─── Higher-level helpers ────────────────────────────────────────────────────
|
|
@@ -117,16 +199,22 @@ export function verifyPacket(packet) {
|
|
|
117
199
|
* Build a packet from a full Cowork template object. The template is reduced
|
|
118
200
|
* to its shareable fields first.
|
|
119
201
|
*/
|
|
120
|
-
export function exportTemplatePacket(
|
|
202
|
+
export function exportTemplatePacket(
|
|
203
|
+
template,
|
|
204
|
+
{ author, cliVersion, signer } = {},
|
|
205
|
+
) {
|
|
121
206
|
const payload = toShareableTemplate(template);
|
|
122
|
-
return buildPacket({ kind: "template", payload, author, cliVersion });
|
|
207
|
+
return buildPacket({ kind: "template", payload, author, cliVersion, signer });
|
|
123
208
|
}
|
|
124
209
|
|
|
125
210
|
/**
|
|
126
211
|
* Build a packet from a history record (one line of history.jsonl).
|
|
127
212
|
* Irrelevant internal fields are dropped.
|
|
128
213
|
*/
|
|
129
|
-
export function exportResultPacket(
|
|
214
|
+
export function exportResultPacket(
|
|
215
|
+
historyRecord,
|
|
216
|
+
{ author, cliVersion, signer } = {},
|
|
217
|
+
) {
|
|
130
218
|
if (!historyRecord || typeof historyRecord !== "object") {
|
|
131
219
|
throw new Error("historyRecord required");
|
|
132
220
|
}
|
|
@@ -139,7 +227,7 @@ export function exportResultPacket(historyRecord, { author, cliVersion } = {}) {
|
|
|
139
227
|
timestamp: historyRecord.timestamp,
|
|
140
228
|
result: historyRecord.result,
|
|
141
229
|
};
|
|
142
|
-
return buildPacket({ kind: "result", payload, author, cliVersion });
|
|
230
|
+
return buildPacket({ kind: "result", payload, author, cliVersion, signer });
|
|
143
231
|
}
|
|
144
232
|
|
|
145
233
|
/**
|
|
@@ -175,7 +263,8 @@ export function writePacket(filePath, packet) {
|
|
|
175
263
|
/**
|
|
176
264
|
* Read + verify a packet from disk. Throws on verification failure.
|
|
177
265
|
*/
|
|
178
|
-
export function readPacket(filePath) {
|
|
266
|
+
export function readPacket(filePath, opts = {}) {
|
|
267
|
+
const { requireSigned = false, trustedDids = null } = opts;
|
|
179
268
|
if (!_deps.existsSync(filePath)) {
|
|
180
269
|
throw new Error(`Packet not found: ${filePath}`);
|
|
181
270
|
}
|
|
@@ -188,6 +277,21 @@ export function readPacket(filePath) {
|
|
|
188
277
|
}
|
|
189
278
|
const { valid, errors } = verifyPacket(packet);
|
|
190
279
|
if (!valid) throw new Error(`Invalid packet: ${errors.join("; ")}`);
|
|
280
|
+
if (requireSigned && !packet.signature) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
"Invalid packet: signature required but packet is unsigned",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
Array.isArray(trustedDids) &&
|
|
287
|
+
trustedDids.length > 0 &&
|
|
288
|
+
packet.signature &&
|
|
289
|
+
!trustedDids.includes(packet.signature.did)
|
|
290
|
+
) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Invalid packet: signer ${packet.signature.did} not in trusted list`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
191
295
|
return packet;
|
|
192
296
|
}
|
|
193
297
|
|