chainlesschain 0.45.81 → 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/bin/chainlesschain.js +0 -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-CXuhlHew.css +1 -0
- package/src/assets/web-panel/assets/Cowork-UmOe7qvE.js +7 -0
- package/src/assets/web-panel/assets/{Cron-DBt1ueXh.js → Cron-JHS-rc-4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-HPh9FcPt.js → Dashboard-B95cMCO7.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
- 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 +867 -0
- package/src/gateways/ws/action-protocol.js +182 -2
- package/src/gateways/ws/message-dispatcher.js +5 -0
- package/src/gateways/ws/ws-server.js +21 -0
- package/src/lib/cowork-cron.js +474 -0
- package/src/lib/cowork-evomap-adapter.js +121 -0
- package/src/lib/cowork-learning.js +438 -0
- package/src/lib/cowork-mcp-tools.js +182 -0
- package/src/lib/cowork-observe-html.js +108 -0
- package/src/lib/cowork-observe.js +160 -0
- package/src/lib/cowork-share.js +322 -0
- package/src/lib/cowork-task-runner.js +317 -3
- package/src/lib/cowork-task-templates.js +101 -13
- package/src/lib/cowork-template-marketplace.js +205 -0
- package/src/lib/cowork-workflow.js +571 -0
- package/src/lib/provider-options.js +133 -0
- package/src/lib/skill-loader.js +65 -0
- package/src/lib/sub-agent-context.js +54 -2
- 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/lib/workflow-expr.js +318 -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-YdvJBMHH.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-cxfKLu-m.css +0 -1
- package/src/assets/web-panel/assets/Cowork-BnrHWwZw.js +0 -7
- package/src/assets/web-panel/assets/Cowork-CcSoS3eX.css +0 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
- package/src/assets/web-panel/assets/index-ByUk2Wmr.js +0 -2
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cowork Share — export/import signed packets for templates and task results.
|
|
3
|
+
*
|
|
4
|
+
* Produces a verifiable JSON packet that can be transferred by any channel
|
|
5
|
+
* (P2P, email, file drop). The packet contains:
|
|
6
|
+
* - `kind`: "template" or "result"
|
|
7
|
+
* - `payload`: the shareable object (template JSON or history record)
|
|
8
|
+
* - `meta`: { author, createdAt, cliVersion }
|
|
9
|
+
* - `checksum`: sha256 hex over the canonicalized payload+meta
|
|
10
|
+
*
|
|
11
|
+
* Import validates the checksum before returning the payload. This is not an
|
|
12
|
+
* identity signature — anyone can produce a packet — but it protects against
|
|
13
|
+
* accidental corruption during transfer.
|
|
14
|
+
*
|
|
15
|
+
* @module cowork-share
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import crypto, { createHash } from "node:crypto";
|
|
21
|
+
import {
|
|
22
|
+
toShareableTemplate,
|
|
23
|
+
saveUserTemplate,
|
|
24
|
+
} from "./cowork-template-marketplace.js";
|
|
25
|
+
import { generateDID } from "./did-manager.js";
|
|
26
|
+
|
|
27
|
+
export const _deps = {
|
|
28
|
+
existsSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
writeFileSync,
|
|
32
|
+
now: () => new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SUPPORTED_SIG_ALG = "Ed25519";
|
|
36
|
+
|
|
37
|
+
const PACKET_VERSION = 1;
|
|
38
|
+
const PACKET_KINDS = ["template", "result"];
|
|
39
|
+
|
|
40
|
+
// ─── Canonical JSON (stable key ordering for checksum) ───────────────────────
|
|
41
|
+
|
|
42
|
+
export function canonicalize(value) {
|
|
43
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return "[" + value.map(canonicalize).join(",") + "]";
|
|
46
|
+
}
|
|
47
|
+
const keys = Object.keys(value).sort();
|
|
48
|
+
return (
|
|
49
|
+
"{" +
|
|
50
|
+
keys
|
|
51
|
+
.map((k) => JSON.stringify(k) + ":" + canonicalize(value[k]))
|
|
52
|
+
.join(",") +
|
|
53
|
+
"}"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sha256Hex(s) {
|
|
58
|
+
return createHash("sha256").update(s, "utf-8").digest("hex");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Packet builders ─────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build a share packet. `kind` and `payload` are validated; `meta` gets
|
|
65
|
+
* filled with createdAt/cliVersion defaults; checksum is computed over the
|
|
66
|
+
* canonical form of `{ kind, version, payload, meta }`.
|
|
67
|
+
*/
|
|
68
|
+
export function buildPacket({
|
|
69
|
+
kind,
|
|
70
|
+
payload,
|
|
71
|
+
author,
|
|
72
|
+
cliVersion,
|
|
73
|
+
signer,
|
|
74
|
+
} = {}) {
|
|
75
|
+
if (!PACKET_KINDS.includes(kind)) {
|
|
76
|
+
throw new Error(`kind must be one of ${PACKET_KINDS.join(", ")}`);
|
|
77
|
+
}
|
|
78
|
+
if (!payload || typeof payload !== "object") {
|
|
79
|
+
throw new Error("payload must be an object");
|
|
80
|
+
}
|
|
81
|
+
const meta = {
|
|
82
|
+
author: author || "anonymous",
|
|
83
|
+
createdAt: _deps.now(),
|
|
84
|
+
cliVersion: cliVersion || "unknown",
|
|
85
|
+
};
|
|
86
|
+
const body = { kind, version: PACKET_VERSION, payload, meta };
|
|
87
|
+
const checksum = sha256Hex(canonicalize(body));
|
|
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
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Verify a packet: checks shape, version, kind, recomputes checksum.
|
|
160
|
+
* Returns `{ valid, errors }`.
|
|
161
|
+
*/
|
|
162
|
+
export function verifyPacket(packet) {
|
|
163
|
+
const errors = [];
|
|
164
|
+
if (!packet || typeof packet !== "object") {
|
|
165
|
+
return { valid: false, errors: ["packet must be an object"] };
|
|
166
|
+
}
|
|
167
|
+
if (packet.version !== PACKET_VERSION) {
|
|
168
|
+
errors.push(`unsupported packet version ${packet.version}`);
|
|
169
|
+
}
|
|
170
|
+
if (!PACKET_KINDS.includes(packet.kind)) {
|
|
171
|
+
errors.push(`unknown kind '${packet.kind}'`);
|
|
172
|
+
}
|
|
173
|
+
if (!packet.payload || typeof packet.payload !== "object") {
|
|
174
|
+
errors.push("payload missing or not an object");
|
|
175
|
+
}
|
|
176
|
+
if (!packet.meta || typeof packet.meta !== "object") {
|
|
177
|
+
errors.push("meta missing");
|
|
178
|
+
}
|
|
179
|
+
if (!packet.checksum) errors.push("checksum missing");
|
|
180
|
+
if (errors.length > 0) return { valid: false, errors };
|
|
181
|
+
|
|
182
|
+
const { checksum, signature, ...body } = packet;
|
|
183
|
+
const expected = sha256Hex(canonicalize(body));
|
|
184
|
+
if (expected !== checksum) {
|
|
185
|
+
errors.push("checksum mismatch (packet may be corrupted or tampered with)");
|
|
186
|
+
}
|
|
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 };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── Higher-level helpers ────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build a packet from a full Cowork template object. The template is reduced
|
|
200
|
+
* to its shareable fields first.
|
|
201
|
+
*/
|
|
202
|
+
export function exportTemplatePacket(
|
|
203
|
+
template,
|
|
204
|
+
{ author, cliVersion, signer } = {},
|
|
205
|
+
) {
|
|
206
|
+
const payload = toShareableTemplate(template);
|
|
207
|
+
return buildPacket({ kind: "template", payload, author, cliVersion, signer });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build a packet from a history record (one line of history.jsonl).
|
|
212
|
+
* Irrelevant internal fields are dropped.
|
|
213
|
+
*/
|
|
214
|
+
export function exportResultPacket(
|
|
215
|
+
historyRecord,
|
|
216
|
+
{ author, cliVersion, signer } = {},
|
|
217
|
+
) {
|
|
218
|
+
if (!historyRecord || typeof historyRecord !== "object") {
|
|
219
|
+
throw new Error("historyRecord required");
|
|
220
|
+
}
|
|
221
|
+
const payload = {
|
|
222
|
+
taskId: historyRecord.taskId,
|
|
223
|
+
status: historyRecord.status,
|
|
224
|
+
templateId: historyRecord.templateId,
|
|
225
|
+
templateName: historyRecord.templateName,
|
|
226
|
+
userMessage: historyRecord.userMessage,
|
|
227
|
+
timestamp: historyRecord.timestamp,
|
|
228
|
+
result: historyRecord.result,
|
|
229
|
+
};
|
|
230
|
+
return buildPacket({ kind: "result", payload, author, cliVersion, signer });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Find a history record by taskId in `.chainlesschain/cowork/history.jsonl`.
|
|
235
|
+
* Returns null if missing. The last matching line wins.
|
|
236
|
+
*/
|
|
237
|
+
export function findHistoryRecord(cwd, taskId) {
|
|
238
|
+
const file = join(cwd, ".chainlesschain", "cowork", "history.jsonl");
|
|
239
|
+
if (!_deps.existsSync(file)) return null;
|
|
240
|
+
const raw = _deps.readFileSync(file, "utf-8");
|
|
241
|
+
let match = null;
|
|
242
|
+
for (const line of raw.split("\n")) {
|
|
243
|
+
const trimmed = line.trim();
|
|
244
|
+
if (!trimmed) continue;
|
|
245
|
+
try {
|
|
246
|
+
const rec = JSON.parse(trimmed);
|
|
247
|
+
if (rec.taskId === taskId) match = rec;
|
|
248
|
+
} catch (_e) {
|
|
249
|
+
// skip malformed
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return match;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Write a packet to disk as pretty-printed JSON.
|
|
257
|
+
*/
|
|
258
|
+
export function writePacket(filePath, packet) {
|
|
259
|
+
_deps.writeFileSync(filePath, JSON.stringify(packet, null, 2), "utf-8");
|
|
260
|
+
return filePath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Read + verify a packet from disk. Throws on verification failure.
|
|
265
|
+
*/
|
|
266
|
+
export function readPacket(filePath, opts = {}) {
|
|
267
|
+
const { requireSigned = false, trustedDids = null } = opts;
|
|
268
|
+
if (!_deps.existsSync(filePath)) {
|
|
269
|
+
throw new Error(`Packet not found: ${filePath}`);
|
|
270
|
+
}
|
|
271
|
+
const body = _deps.readFileSync(filePath, "utf-8");
|
|
272
|
+
let packet;
|
|
273
|
+
try {
|
|
274
|
+
packet = JSON.parse(body);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
throw new Error(`Packet is not valid JSON: ${err.message}`);
|
|
277
|
+
}
|
|
278
|
+
const { valid, errors } = verifyPacket(packet);
|
|
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
|
+
}
|
|
295
|
+
return packet;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Import a template packet into the local marketplace.
|
|
300
|
+
* Returns the installed template.
|
|
301
|
+
*/
|
|
302
|
+
export function importTemplatePacket(cwd, packet) {
|
|
303
|
+
if (packet.kind !== "template") {
|
|
304
|
+
throw new Error(`Expected template packet, got '${packet.kind}'`);
|
|
305
|
+
}
|
|
306
|
+
return saveUserTemplate(cwd, packet.payload);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Import a result packet into a local `.chainlesschain/cowork/shared-results/`
|
|
311
|
+
* directory. Produces one JSON file per result, keyed by taskId.
|
|
312
|
+
*/
|
|
313
|
+
export function importResultPacket(cwd, packet) {
|
|
314
|
+
if (packet.kind !== "result") {
|
|
315
|
+
throw new Error(`Expected result packet, got '${packet.kind}'`);
|
|
316
|
+
}
|
|
317
|
+
const dir = join(cwd, ".chainlesschain", "cowork", "shared-results");
|
|
318
|
+
_deps.mkdirSync(dir, { recursive: true });
|
|
319
|
+
const file = join(dir, `${packet.payload.taskId}.json`);
|
|
320
|
+
_deps.writeFileSync(file, JSON.stringify(packet, null, 2), "utf-8");
|
|
321
|
+
return { file, taskId: packet.payload.taskId };
|
|
322
|
+
}
|