@nema-project/nema 0.1.4 → 0.1.6
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/dist/chunks/{chunk-HEIQINX3.js → chunk-W4DUBATU.js} +231 -91
- package/dist/daemon/connection.js +1 -1
- package/dist/direct/tools.js +1 -1
- package/dist/extensions/handlers.d.ts +16 -1
- package/dist/extensions/handlers.js +77 -2
- package/dist/extensions/install.d.ts +70 -0
- package/dist/extensions/install.js +241 -0
- package/dist/extensions/source.d.ts +81 -0
- package/dist/extensions/source.js +203 -0
- package/dist/extensions/store.d.ts +7 -0
- package/dist/extensions/store.js +72 -5
- package/dist/github/client.js +1 -1
- package/dist/github/gh.js +1 -1
- package/dist/report/commands.js +1 -1
- package/dist/report/desktop.js +11 -4
- package/dist/repos/git.js +1 -1
- package/dist/repos/handlers.js +1 -1
- package/package.json +1 -1
|
@@ -32,12 +32,49 @@ var Implementation = z.object({
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
// ../../../packages/shared/dist/entry.js
|
|
35
|
+
import { z as z3 } from "zod";
|
|
36
|
+
|
|
37
|
+
// ../../../packages/shared/dist/sourceEvidence.js
|
|
35
38
|
import { z as z2 } from "zod";
|
|
39
|
+
var NoControls = /^[^\x00-\x1f\x7f]*$/;
|
|
40
|
+
var SourceRevision = z2.string().min(1).max(256).regex(NoControls).nullable();
|
|
41
|
+
var SnapshotHash = z2.string().regex(/^[0-9a-f]{64}$/);
|
|
42
|
+
var SourceExcerpt = z2.object({
|
|
43
|
+
locator: z2.string().min(1).max(200),
|
|
44
|
+
text: z2.string().min(1).max(2e3)
|
|
45
|
+
});
|
|
46
|
+
var SourceSnapshot = z2.object({
|
|
47
|
+
schemaVersion: z2.literal(1),
|
|
48
|
+
provider: z2.string().regex(/^[a-z][a-z0-9_-]{0,63}$/),
|
|
49
|
+
resourceId: z2.string().min(1).max(512).regex(NoControls).refine((value) => value.trim() === value, "resourceId must not have surrounding whitespace"),
|
|
50
|
+
revision: SourceRevision,
|
|
51
|
+
fetchedAt: z2.iso.datetime({ offset: true }),
|
|
52
|
+
title: z2.string().min(1).max(200),
|
|
53
|
+
canonicalUrl: z2.string().refine((value) => {
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(value);
|
|
56
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "";
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}, "canonicalUrl must be http or https without user information").nullable(),
|
|
61
|
+
contentHash: SnapshotHash,
|
|
62
|
+
excerpts: z2.array(SourceExcerpt).min(1).max(20).refine((excerpts) => excerpts.reduce((total, excerpt) => total + excerpt.text.length, 0) <= 8e3, "excerpt text must total at most 8000 characters")
|
|
63
|
+
});
|
|
64
|
+
var SourceRef = z2.object({
|
|
65
|
+
sourceEntryId: z2.string().regex(/^S-[1-9]\d*$/),
|
|
66
|
+
revision: SourceRevision,
|
|
67
|
+
snapshotHash: SnapshotHash,
|
|
68
|
+
locator: z2.string().min(1).max(200).optional(),
|
|
69
|
+
quote: z2.string().min(1).max(500).optional()
|
|
70
|
+
}).refine((ref) => ref.quote === void 0 || ref.locator !== void 0, { path: ["locator"], message: "locator is required with quote" });
|
|
71
|
+
|
|
72
|
+
// ../../../packages/shared/dist/entry.js
|
|
36
73
|
var ENTRY_KINDS = ["requirement", "decision", "tradeoff", "question", "source", "diagram", "workpackage"];
|
|
37
|
-
var EntryKindSchema =
|
|
74
|
+
var EntryKindSchema = z3.enum(ENTRY_KINDS);
|
|
38
75
|
var BASIS_VALUES = ["stated", "decided", "inferred"];
|
|
39
|
-
var BasisSchema =
|
|
40
|
-
var Quote =
|
|
76
|
+
var BasisSchema = z3.enum(BASIS_VALUES);
|
|
77
|
+
var Quote = z3.object({ messageId: z3.string().min(1), text: z3.string().min(1).max(500) });
|
|
41
78
|
var DIAGRAM_TYPES = [
|
|
42
79
|
"architecture",
|
|
43
80
|
"workflow",
|
|
@@ -50,34 +87,34 @@ var DIAGRAM_TYPES = [
|
|
|
50
87
|
"timeline",
|
|
51
88
|
"waterfall"
|
|
52
89
|
];
|
|
53
|
-
var DiagramMeta =
|
|
54
|
-
repoId:
|
|
55
|
-
type:
|
|
90
|
+
var DiagramMeta = z3.object({
|
|
91
|
+
repoId: z3.string().min(1),
|
|
92
|
+
type: z3.enum(DIAGRAM_TYPES),
|
|
56
93
|
/** repo id -> main sha the diagram was drawn at. */
|
|
57
|
-
sourceCommits:
|
|
94
|
+
sourceCommits: z3.record(z3.string(), z3.string()),
|
|
58
95
|
/** archify node id -> repo-relative paths that node stands for. */
|
|
59
|
-
nodePaths:
|
|
96
|
+
nodePaths: z3.record(z3.string(), z3.array(z3.string()))
|
|
60
97
|
});
|
|
61
98
|
var MAX_WP_TASKS = 15;
|
|
62
|
-
var WpTask =
|
|
63
|
-
n:
|
|
64
|
-
title:
|
|
65
|
-
satisfies:
|
|
66
|
-
files:
|
|
67
|
-
status:
|
|
68
|
-
inferred:
|
|
69
|
-
reportMessageId:
|
|
70
|
-
confirmedBy:
|
|
99
|
+
var WpTask = z3.object({
|
|
100
|
+
n: z3.number().int().min(1).max(MAX_WP_TASKS),
|
|
101
|
+
title: z3.string(),
|
|
102
|
+
satisfies: z3.array(z3.string()),
|
|
103
|
+
files: z3.array(z3.object({ op: z3.enum(["modify", "create"]), repo: z3.string(), path: z3.string() })),
|
|
104
|
+
status: z3.enum(["pending", "reported", "verified", "mismatch"]),
|
|
105
|
+
inferred: z3.boolean(),
|
|
106
|
+
reportMessageId: z3.string().nullable(),
|
|
107
|
+
confirmedBy: z3.literal("user").nullable()
|
|
71
108
|
});
|
|
72
|
-
var WorkPackageMeta =
|
|
73
|
-
requirements:
|
|
74
|
-
repos:
|
|
75
|
-
approvedAt:
|
|
76
|
-
approvedCommit:
|
|
77
|
-
blockedOn:
|
|
78
|
-
tasks:
|
|
109
|
+
var WorkPackageMeta = z3.object({
|
|
110
|
+
requirements: z3.array(z3.string()),
|
|
111
|
+
repos: z3.array(z3.string()),
|
|
112
|
+
approvedAt: z3.string().nullable(),
|
|
113
|
+
approvedCommit: z3.string().nullable(),
|
|
114
|
+
blockedOn: z3.string().nullable(),
|
|
115
|
+
tasks: z3.array(WpTask)
|
|
79
116
|
});
|
|
80
|
-
var IsoDateTime =
|
|
117
|
+
var IsoDateTime = z3.iso.datetime({ offset: true }).nullable();
|
|
81
118
|
var PREFIX_KIND = {
|
|
82
119
|
R: "requirement",
|
|
83
120
|
D: "decision",
|
|
@@ -103,9 +140,9 @@ var KIND_STATUSES = {
|
|
|
103
140
|
workpackage: ["draft", "approved", "in_progress", "done", "dropped"]
|
|
104
141
|
};
|
|
105
142
|
var ALL_STATUSES = ["active", "superseded", "dropped", "open", "resolved", "draft", "approved", "in_progress", "done"];
|
|
106
|
-
var StatusSchema =
|
|
107
|
-
var BusinessDate =
|
|
108
|
-
var HttpUrl =
|
|
143
|
+
var StatusSchema = z3.enum(ALL_STATUSES);
|
|
144
|
+
var BusinessDate = z3.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable();
|
|
145
|
+
var HttpUrl = z3.string().refine((s) => {
|
|
109
146
|
try {
|
|
110
147
|
const p = new URL(s).protocol;
|
|
111
148
|
return p === "http:" || p === "https:";
|
|
@@ -113,17 +150,23 @@ var HttpUrl = z2.string().refine((s) => {
|
|
|
113
150
|
return false;
|
|
114
151
|
}
|
|
115
152
|
}, "url must be http or https");
|
|
116
|
-
var Priority =
|
|
153
|
+
var Priority = z3.union([z3.literal(1), z3.literal(2), z3.literal(3)]);
|
|
117
154
|
function refineKind(v, ctx) {
|
|
118
155
|
if (v.status !== void 0 && !KIND_STATUSES[v.kind].includes(v.status)) {
|
|
119
156
|
ctx.addIssue({ code: "custom", path: ["status"], message: `invalid status "${v.status}" for ${v.kind}` });
|
|
120
157
|
}
|
|
121
158
|
if (v.kind === "source") {
|
|
122
|
-
if (v.url === void 0)
|
|
159
|
+
if (v.url === void 0 && v.sourceSnapshot === void 0)
|
|
123
160
|
ctx.addIssue({ code: "custom", path: ["url"], message: "url is required for source" });
|
|
124
161
|
} else if (v.url !== void 0) {
|
|
125
162
|
ctx.addIssue({ code: "custom", path: ["url"], message: "url is only allowed for source" });
|
|
126
163
|
}
|
|
164
|
+
if (v.sourceSnapshot !== void 0 && v.kind !== "source") {
|
|
165
|
+
ctx.addIssue({ code: "custom", path: ["sourceSnapshot"], message: "sourceSnapshot is only allowed for source" });
|
|
166
|
+
}
|
|
167
|
+
if (v.confirmedRevision != null && (v.kind !== "source" || v.sourceSnapshot === void 0)) {
|
|
168
|
+
ctx.addIssue({ code: "custom", path: ["confirmedRevision"], message: "confirmedRevision requires a source snapshot" });
|
|
169
|
+
}
|
|
127
170
|
if (v.kind === "diagram") {
|
|
128
171
|
if (v.diagram === void 0)
|
|
129
172
|
ctx.addIssue({ code: "custom", path: ["diagram"], message: "diagram is required for diagram" });
|
|
@@ -146,14 +189,17 @@ function refineKind(v, ctx) {
|
|
|
146
189
|
ctx.addIssue({ code: "custom", path: ["priority"], message: "priority is only allowed for question" });
|
|
147
190
|
}
|
|
148
191
|
}
|
|
149
|
-
var EntryFrontmatter =
|
|
150
|
-
id:
|
|
192
|
+
var EntryFrontmatter = z3.object({
|
|
193
|
+
id: z3.string().regex(ID_RE),
|
|
151
194
|
kind: EntryKindSchema,
|
|
152
|
-
title:
|
|
195
|
+
title: z3.string().min(1).max(200),
|
|
153
196
|
status: StatusSchema,
|
|
154
197
|
businessDate: BusinessDate,
|
|
155
|
-
related:
|
|
156
|
-
sourceMessages:
|
|
198
|
+
related: z3.array(z3.string()),
|
|
199
|
+
sourceMessages: z3.array(z3.string()),
|
|
200
|
+
sourceSnapshot: SourceSnapshot.optional(),
|
|
201
|
+
sourceRefs: z3.array(SourceRef).max(10).default([]),
|
|
202
|
+
confirmedRevision: SourceRevision.default(null),
|
|
157
203
|
url: HttpUrl.optional(),
|
|
158
204
|
priority: Priority.optional(),
|
|
159
205
|
diagram: DiagramMeta.optional(),
|
|
@@ -162,30 +208,31 @@ var EntryFrontmatter = z2.object({
|
|
|
162
208
|
basis: BasisSchema.nullable(),
|
|
163
209
|
ingestedAt: IsoDateTime,
|
|
164
210
|
confirmedAt: IsoDateTime,
|
|
165
|
-
quotes:
|
|
211
|
+
quotes: z3.array(Quote)
|
|
166
212
|
}).superRefine((v, ctx) => {
|
|
167
213
|
refineKind(v, ctx);
|
|
168
214
|
if (kindFromId(v.id) !== v.kind) {
|
|
169
215
|
ctx.addIssue({ code: "custom", path: ["id"], message: "id prefix does not match kind" });
|
|
170
216
|
}
|
|
171
217
|
});
|
|
172
|
-
var UpsertEntryInput =
|
|
173
|
-
id:
|
|
218
|
+
var UpsertEntryInput = z3.object({
|
|
219
|
+
id: z3.string().regex(ID_RE).optional(),
|
|
174
220
|
kind: EntryKindSchema,
|
|
175
|
-
title:
|
|
176
|
-
body:
|
|
221
|
+
title: z3.string().min(1).max(200),
|
|
222
|
+
body: z3.string(),
|
|
177
223
|
status: StatusSchema.optional(),
|
|
178
224
|
businessDate: BusinessDate.optional(),
|
|
179
|
-
related:
|
|
180
|
-
sourceMessages:
|
|
225
|
+
related: z3.array(z3.string()).optional(),
|
|
226
|
+
sourceMessages: z3.array(z3.string()).default([]),
|
|
227
|
+
sourceRefs: z3.array(SourceRef).max(10).optional(),
|
|
181
228
|
url: HttpUrl.optional(),
|
|
182
229
|
priority: Priority.optional(),
|
|
183
230
|
/** Requirements only. The Room API checks it against reports; omitted keeps the stored one. */
|
|
184
231
|
implementation: Implementation.optional(),
|
|
185
232
|
basis: BasisSchema.optional(),
|
|
186
|
-
quotes:
|
|
187
|
-
confirmed:
|
|
188
|
-
reason:
|
|
233
|
+
quotes: z3.array(Quote).max(10).optional(),
|
|
234
|
+
confirmed: z3.boolean().optional(),
|
|
235
|
+
reason: z3.string().min(1).max(500)
|
|
189
236
|
}).superRefine((v, ctx) => {
|
|
190
237
|
refineKind(v, ctx);
|
|
191
238
|
if (v.id !== void 0 && kindFromId(v.id) !== v.kind) {
|
|
@@ -194,23 +241,23 @@ var UpsertEntryInput = z2.object({
|
|
|
194
241
|
});
|
|
195
242
|
|
|
196
243
|
// ../../../packages/shared/dist/workspace.js
|
|
197
|
-
import { z as
|
|
198
|
-
var MessageQuote =
|
|
199
|
-
messageId:
|
|
200
|
-
role:
|
|
201
|
-
excerpt:
|
|
244
|
+
import { z as z4 } from "zod";
|
|
245
|
+
var MessageQuote = z4.object({
|
|
246
|
+
messageId: z4.string(),
|
|
247
|
+
role: z4.enum(["user", "agent", "system", "report"]),
|
|
248
|
+
excerpt: z4.string()
|
|
202
249
|
});
|
|
203
|
-
var UserMessageMeta =
|
|
250
|
+
var UserMessageMeta = z4.object({
|
|
204
251
|
quote: MessageQuote.optional()
|
|
205
252
|
});
|
|
206
|
-
var AgentMessageMeta =
|
|
207
|
-
changes:
|
|
208
|
-
entryId:
|
|
253
|
+
var AgentMessageMeta = z4.object({
|
|
254
|
+
changes: z4.array(z4.object({
|
|
255
|
+
entryId: z4.string(),
|
|
209
256
|
kind: EntryKindSchema,
|
|
210
|
-
title:
|
|
211
|
-
op:
|
|
257
|
+
title: z4.string(),
|
|
258
|
+
op: z4.enum(["add", "update"])
|
|
212
259
|
})),
|
|
213
|
-
askedQuestion:
|
|
260
|
+
askedQuestion: z4.object({ id: z4.string(), title: z4.string(), body: z4.string() }).nullable(),
|
|
214
261
|
quote: MessageQuote.optional()
|
|
215
262
|
});
|
|
216
263
|
function isBranchName(name) {
|
|
@@ -626,49 +673,140 @@ function bundledExtensions() {
|
|
|
626
673
|
];
|
|
627
674
|
}
|
|
628
675
|
|
|
676
|
+
// ../../../packages/shared/dist/extensionSource.js
|
|
677
|
+
var MAX_INPUT = 2048;
|
|
678
|
+
var OWNER2 = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
679
|
+
var REPO = /^[A-Za-z0-9._-]{1,100}$/;
|
|
680
|
+
var SEGMENT = /^[A-Za-z0-9._-]{1,255}$/;
|
|
681
|
+
var REF = /^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$/;
|
|
682
|
+
function refuse(input) {
|
|
683
|
+
throw new Error(`unsupported extension source: ${input.length > 200 ? `${input.slice(0, 200)}\u2026` : input}`);
|
|
684
|
+
}
|
|
685
|
+
function plainSegment(segment) {
|
|
686
|
+
return SEGMENT.test(segment) && segment !== "." && segment !== "..";
|
|
687
|
+
}
|
|
688
|
+
function repoName(segment) {
|
|
689
|
+
const name = segment.endsWith(".git") ? segment.slice(0, -4) : segment;
|
|
690
|
+
return REPO.test(name) && name !== "." && name !== ".." ? name : null;
|
|
691
|
+
}
|
|
692
|
+
function parseSkillSource(raw) {
|
|
693
|
+
const input = raw.trim();
|
|
694
|
+
if (!input || input.length > MAX_INPUT)
|
|
695
|
+
refuse(input);
|
|
696
|
+
if (!input.includes(":") && !input.startsWith("github.com/") && /^[^/]+\/[^/]+$/.test(input)) {
|
|
697
|
+
const [owner2, repo2] = input.split("/");
|
|
698
|
+
const name = repoName(repo2);
|
|
699
|
+
if (!OWNER2.test(owner2) || !name)
|
|
700
|
+
refuse(input);
|
|
701
|
+
return { via: "github", owner: owner2, repo: name, ref: null, path: null, skill: null, refPath: null, exact: false };
|
|
702
|
+
}
|
|
703
|
+
let url;
|
|
704
|
+
try {
|
|
705
|
+
url = new URL(input.startsWith("github.com/") ? `https://${input}` : input);
|
|
706
|
+
} catch {
|
|
707
|
+
refuse(input);
|
|
708
|
+
}
|
|
709
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port || url.search || url.hash)
|
|
710
|
+
refuse(input);
|
|
711
|
+
const typedPath = input.replace(/^https:\/\/[^/]+/, "").replace(/^github\.com/, "");
|
|
712
|
+
if (typedPath !== url.pathname)
|
|
713
|
+
refuse(input);
|
|
714
|
+
const segments = url.pathname.replace(/\/$/, "").split("/").slice(1);
|
|
715
|
+
if (segments.some((part) => part === ""))
|
|
716
|
+
refuse(input);
|
|
717
|
+
const host = url.hostname.toLowerCase();
|
|
718
|
+
if (host === "skills.sh" || host === "www.skills.sh") {
|
|
719
|
+
if (segments.length < 2 || segments.length > 3 || !segments.every(plainSegment))
|
|
720
|
+
refuse(input);
|
|
721
|
+
const name = repoName(segments[1]);
|
|
722
|
+
if (!OWNER2.test(segments[0]) || !name)
|
|
723
|
+
refuse(input);
|
|
724
|
+
return { via: "skills.sh", owner: segments[0], repo: name, ref: null, path: null, skill: segments[2] ?? null, refPath: null, exact: false };
|
|
725
|
+
}
|
|
726
|
+
if (host !== "github.com" && host !== "www.github.com")
|
|
727
|
+
refuse(input);
|
|
728
|
+
if (segments.length < 2)
|
|
729
|
+
refuse(input);
|
|
730
|
+
const [owner, repoSegment, kind, ref, ...rest] = segments;
|
|
731
|
+
const repo = repoName(repoSegment);
|
|
732
|
+
if (!OWNER2.test(owner) || !repo)
|
|
733
|
+
refuse(input);
|
|
734
|
+
if (kind === void 0)
|
|
735
|
+
return { via: "github", owner, repo, ref: null, path: null, skill: null, refPath: null, exact: false };
|
|
736
|
+
if (kind !== "tree" && kind !== "blob" || ref === void 0 || !REF.test(ref) || ref.includes(".."))
|
|
737
|
+
refuse(input);
|
|
738
|
+
if (!rest.every(plainSegment))
|
|
739
|
+
refuse(input);
|
|
740
|
+
let folder = rest;
|
|
741
|
+
if (kind === "blob") {
|
|
742
|
+
if (rest.at(-1) !== "SKILL.md")
|
|
743
|
+
refuse(input);
|
|
744
|
+
folder = rest.slice(0, -1);
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
via: "github",
|
|
748
|
+
owner,
|
|
749
|
+
repo,
|
|
750
|
+
ref,
|
|
751
|
+
path: folder.length ? folder.join("/") : null,
|
|
752
|
+
skill: null,
|
|
753
|
+
refPath: [ref, ...folder].join("/"),
|
|
754
|
+
exact: kind === "blob"
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
var UNSAFE_NAME = /[\\/:*?"<>|\u0000-\u001f]/;
|
|
758
|
+
var WINDOWS_DEVICE = /^(con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³])(\..*)?$/i;
|
|
759
|
+
function safeFileName(name) {
|
|
760
|
+
return name !== "" && name !== "." && name !== ".." && name.length <= 255 && !UNSAFE_NAME.test(name) && !WINDOWS_DEVICE.test(name) && // Windows drops a trailing dot or space, so `a.` and `a` would be the same file there.
|
|
761
|
+
!/[. ]$/.test(name);
|
|
762
|
+
}
|
|
763
|
+
function safeRepoPath(relative) {
|
|
764
|
+
return relative.split("/").every(safeFileName);
|
|
765
|
+
}
|
|
766
|
+
|
|
629
767
|
// ../../../packages/shared/dist/protocol.js
|
|
630
|
-
import { z as
|
|
631
|
-
var DaemonToHubSchema =
|
|
632
|
-
|
|
633
|
-
type:
|
|
634
|
-
token:
|
|
635
|
-
version:
|
|
636
|
-
adapters:
|
|
768
|
+
import { z as z5 } from "zod";
|
|
769
|
+
var DaemonToHubSchema = z5.union([
|
|
770
|
+
z5.object({
|
|
771
|
+
type: z5.literal("hello"),
|
|
772
|
+
token: z5.string(),
|
|
773
|
+
version: z5.string(),
|
|
774
|
+
adapters: z5.array(z5.string()),
|
|
637
775
|
/** sha256 hex of ~/.nema/report-token, sent when that file exists. */
|
|
638
|
-
reportTokenHash:
|
|
776
|
+
reportTokenHash: z5.string().regex(/^[0-9a-f]{64}$/).optional()
|
|
639
777
|
}),
|
|
640
|
-
|
|
641
|
-
type:
|
|
642
|
-
taskId:
|
|
643
|
-
event:
|
|
778
|
+
z5.object({
|
|
779
|
+
type: z5.literal("task.event"),
|
|
780
|
+
taskId: z5.string(),
|
|
781
|
+
event: z5.object({ type: z5.enum(["progress", "agent_message", "tool_call"]), text: z5.string() })
|
|
644
782
|
}),
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
783
|
+
z5.object({ type: z5.literal("task.finished"), taskId: z5.string(), ok: z5.literal(true), reply: z5.string() }),
|
|
784
|
+
z5.object({ type: z5.literal("task.finished"), taskId: z5.string(), ok: z5.literal(false), error: z5.string() }),
|
|
785
|
+
z5.object({ type: z5.literal("rpc.response"), id: z5.string(), ok: z5.literal(true), result: z5.unknown() }),
|
|
786
|
+
z5.object({ type: z5.literal("rpc.response"), id: z5.string(), ok: z5.literal(false), error: z5.string() }),
|
|
787
|
+
z5.object({ type: z5.literal("repo.moved"), repoId: z5.string(), mainRef: z5.string(), from: z5.string().nullable(), to: z5.string() }),
|
|
788
|
+
z5.object({ type: z5.literal("repo.fetched"), repoId: z5.string(), ok: z5.boolean(), error: z5.string().optional(), at: z5.string() })
|
|
651
789
|
]);
|
|
652
|
-
var HubToDaemonSchema =
|
|
790
|
+
var HubToDaemonSchema = z5.discriminatedUnion("type", [
|
|
653
791
|
// workspaceIds: every workspace that still exists, so the daemon can remove the isolation dirs
|
|
654
792
|
// (`~/.nema/ws/<id>`) of workspaces deleted while it was offline. Optional for older hubs.
|
|
655
|
-
|
|
793
|
+
z5.object({ type: z5.literal("welcome"), workspaceIds: z5.array(z5.string()).optional() }),
|
|
656
794
|
// Sent to a connected daemon when a workspace is deleted, so it removes that workspace's isolation dir.
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
type:
|
|
660
|
-
taskId:
|
|
661
|
-
workspaceId:
|
|
662
|
-
adapter:
|
|
663
|
-
confidential:
|
|
664
|
-
prompt:
|
|
665
|
-
runToken:
|
|
666
|
-
hubUrl:
|
|
667
|
-
pluginTools:
|
|
668
|
-
limits:
|
|
795
|
+
z5.object({ type: z5.literal("workspace.deleted"), workspaceId: z5.string() }),
|
|
796
|
+
z5.object({
|
|
797
|
+
type: z5.literal("task.assign"),
|
|
798
|
+
taskId: z5.string(),
|
|
799
|
+
workspaceId: z5.string(),
|
|
800
|
+
adapter: z5.string().min(1),
|
|
801
|
+
confidential: z5.boolean(),
|
|
802
|
+
prompt: z5.string(),
|
|
803
|
+
runToken: z5.string(),
|
|
804
|
+
hubUrl: z5.string(),
|
|
805
|
+
pluginTools: z5.array(z5.object({ name: z5.string(), description: z5.string(), parameters: z5.record(z5.string(), z5.unknown()) })),
|
|
806
|
+
limits: z5.object({ maxModelCalls: z5.number().int().min(1), maxToolCalls: z5.number().int().min(1), timeoutMs: z5.number().int().min(1) }).optional()
|
|
669
807
|
}),
|
|
670
|
-
|
|
671
|
-
|
|
808
|
+
z5.object({ type: z5.literal("rpc.request"), id: z5.string(), method: z5.string(), params: z5.unknown() }),
|
|
809
|
+
z5.object({ type: z5.literal("error"), message: z5.string() })
|
|
672
810
|
]);
|
|
673
811
|
function parseHubToDaemon(raw) {
|
|
674
812
|
return HubToDaemonSchema.parse(raw);
|
|
@@ -819,6 +957,8 @@ export {
|
|
|
819
957
|
isBranchName,
|
|
820
958
|
PLUGINS,
|
|
821
959
|
bundledExtensions,
|
|
960
|
+
parseSkillSource,
|
|
961
|
+
safeRepoPath,
|
|
822
962
|
parseHubToDaemon,
|
|
823
963
|
parseGhCommand,
|
|
824
964
|
GH_COMMAND_TOOLS
|
package/dist/direct/tools.js
CHANGED
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
import type { RpcRouter } from "../daemon/rpc.js";
|
|
2
|
-
|
|
2
|
+
import { type SkillCandidate, type SkillResolution, type SourceFetch } from "./source.js";
|
|
3
|
+
export type ExtensionRemoval = {
|
|
4
|
+
id: string;
|
|
5
|
+
removed: "managed" | "external";
|
|
6
|
+
};
|
|
7
|
+
/** A resolved source as the install preview shows it: where each skill would land, and whether it is already there. */
|
|
8
|
+
export type SourcePreview = SkillResolution & {
|
|
9
|
+
skills: Array<SkillCandidate & {
|
|
10
|
+
target: string | null;
|
|
11
|
+
installed: boolean;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
export type ExtensionHandlerOptions = {
|
|
15
|
+
fetch?: SourceFetch;
|
|
16
|
+
};
|
|
17
|
+
export declare function registerExtensionHandlers(router: RpcRouter, nemaHome: string, options?: ExtensionHandlerOptions): void;
|
|
@@ -1,9 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseSkillSource
|
|
3
|
+
} from "../chunks/chunk-W4DUBATU.js";
|
|
4
|
+
|
|
1
5
|
// extensions/handlers.js
|
|
2
6
|
import path from "node:path";
|
|
7
|
+
import { SkillInstaller } from "./install.js";
|
|
8
|
+
import { resolveSkillSource } from "./source.js";
|
|
3
9
|
import { openExtensionStore } from "./store.js";
|
|
4
|
-
|
|
5
|
-
|
|
10
|
+
var RESOLVE_TIMEOUT_MS = 45e3;
|
|
11
|
+
function requireId(params, method) {
|
|
12
|
+
const id = params?.id;
|
|
13
|
+
if (typeof id !== "string" || !id)
|
|
14
|
+
throw new Error(`${method} needs a string id`);
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
function requireUseState(params) {
|
|
18
|
+
const useState = params?.useState;
|
|
19
|
+
if (useState !== "enabled" && useState !== "disabled")
|
|
20
|
+
throw new Error("extensions.setUse needs useState enabled or disabled");
|
|
21
|
+
return useState;
|
|
22
|
+
}
|
|
23
|
+
function registerExtensionHandlers(router, nemaHome, options = {}) {
|
|
24
|
+
const root = path.join(nemaHome, "extensions");
|
|
25
|
+
const store = openExtensionStore(root);
|
|
26
|
+
const fetch = options.fetch ?? globalThis.fetch;
|
|
27
|
+
const installer = new SkillInstaller({ root, store, fetch });
|
|
28
|
+
installer.recover();
|
|
6
29
|
router.register("extensions.list", async () => store.list());
|
|
30
|
+
router.register("extensions.setUse", async (params) => store.setUseState(requireId(params, "extensions.setUse"), requireUseState(params)));
|
|
31
|
+
router.register("extensions.remove", async (params) => {
|
|
32
|
+
const id = requireId(params, "extensions.remove");
|
|
33
|
+
const record = store.list().find((row) => row.id === id);
|
|
34
|
+
if (!record)
|
|
35
|
+
throw new Error(`extension not found: ${id}`);
|
|
36
|
+
if (installer.running(id))
|
|
37
|
+
throw new Error(`${id} is installing; cancel the install and wait for it to stop first`);
|
|
38
|
+
if (record.provision === "bundled")
|
|
39
|
+
throw new Error("bundled extensions cannot be removed");
|
|
40
|
+
if (record.provision === "managed") {
|
|
41
|
+
store.removeManaged(id);
|
|
42
|
+
installer.forget(id);
|
|
43
|
+
return { id, removed: "managed" };
|
|
44
|
+
}
|
|
45
|
+
store.detach(id);
|
|
46
|
+
return { id, removed: "external" };
|
|
47
|
+
});
|
|
48
|
+
router.register("extensions.resolveSource", async (params) => {
|
|
49
|
+
const raw = params?.source;
|
|
50
|
+
if (typeof raw !== "string")
|
|
51
|
+
throw new Error("extensions.resolveSource needs a string source");
|
|
52
|
+
const source = parseSkillSource(raw);
|
|
53
|
+
let resolved;
|
|
54
|
+
try {
|
|
55
|
+
resolved = await resolveSkillSource(source, { fetch, signal: AbortSignal.timeout(RESOLVE_TIMEOUT_MS) });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
|
|
58
|
+
throw new Error(`GitHub did not answer within ${RESOLVE_TIMEOUT_MS / 1e3} s`);
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const present = new Set(store.list().filter((row) => row.installState === "installed").map((row) => row.id));
|
|
63
|
+
return {
|
|
64
|
+
...resolved,
|
|
65
|
+
skills: resolved.skills.map((skill) => ({
|
|
66
|
+
...skill,
|
|
67
|
+
target: skill.id ? path.join(root, "extensions", skill.id) : null,
|
|
68
|
+
installed: skill.id ? present.has(skill.id) : false
|
|
69
|
+
}))
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
router.register("extensions.install", async (params) => {
|
|
73
|
+
const p = params ?? {};
|
|
74
|
+
if (typeof p.source !== "string" || typeof p.commit !== "string" || typeof p.path !== "string") {
|
|
75
|
+
throw new Error("extensions.install needs string source, commit and path");
|
|
76
|
+
}
|
|
77
|
+
return installer.start({ source: p.source, commit: p.commit, path: p.path });
|
|
78
|
+
});
|
|
79
|
+
router.register("extensions.installStatus", async (params) => installer.status(requireId(params, "extensions.installStatus")));
|
|
80
|
+
router.register("extensions.cancelInstall", async (params) => installer.cancel(requireId(params, "extensions.cancelInstall")));
|
|
81
|
+
router.register("extensions.retryInstall", async (params) => installer.retry(requireId(params, "extensions.retryInstall")));
|
|
7
82
|
}
|
|
8
83
|
export {
|
|
9
84
|
registerExtensionHandlers
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type SourceFetch, type TreeEntry } from "./source.js";
|
|
2
|
+
import type { openExtensionStore } from "./store.js";
|
|
3
|
+
type Store = ReturnType<typeof openExtensionStore>;
|
|
4
|
+
export type InstallState = "running" | "installed" | "failed" | "cancelled";
|
|
5
|
+
/** What the page polls while a skill installs, and what it reads afterwards. */
|
|
6
|
+
export type InstallJob = {
|
|
7
|
+
id: string;
|
|
8
|
+
state: InstallState;
|
|
9
|
+
/** Files written to staging so far, out of `total`. */
|
|
10
|
+
done: number;
|
|
11
|
+
total: number;
|
|
12
|
+
error: string | null;
|
|
13
|
+
commit: string | null;
|
|
14
|
+
path: string | null;
|
|
15
|
+
};
|
|
16
|
+
export type InstallRequest = {
|
|
17
|
+
source: string;
|
|
18
|
+
commit: string;
|
|
19
|
+
path: string;
|
|
20
|
+
};
|
|
21
|
+
export declare const INTERRUPTED = "interrupted: the daemon stopped before the install finished";
|
|
22
|
+
/**
|
|
23
|
+
* The files of one skill folder at one commit, checked before any byte is downloaded: regular files
|
|
24
|
+
* only, names safe on every platform, no two names that collide on a case-insensitive disk, and
|
|
25
|
+
* inside the count and size limits.
|
|
26
|
+
*/
|
|
27
|
+
export declare function skillFiles(entries: TreeEntry[], folder: string): Array<TreeEntry & {
|
|
28
|
+
relative: string;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Installs one skill folder from a pinned commit into Nema's managed directory.
|
|
32
|
+
*
|
|
33
|
+
* The record goes in first as `installing`, carrying the commit and folder, so a crash leaves enough
|
|
34
|
+
* behind for `recover()` to mark it failed and for `retry()` to start again from the same commit.
|
|
35
|
+
* Files are downloaded into `<root>/staging/<id>-*`, each checked against its Git blob hash, and only
|
|
36
|
+
* a complete, verified folder is handed to `installManaged`, which swaps it in.
|
|
37
|
+
*/
|
|
38
|
+
export declare class SkillInstaller {
|
|
39
|
+
private readonly o;
|
|
40
|
+
private readonly jobs;
|
|
41
|
+
/** Ids a retry has claimed while it re-reads the recorded commit, before its job exists. */
|
|
42
|
+
private readonly reserved;
|
|
43
|
+
private readonly staging;
|
|
44
|
+
constructor(o: {
|
|
45
|
+
root: string;
|
|
46
|
+
store: Store;
|
|
47
|
+
fetch: SourceFetch;
|
|
48
|
+
downloadTimeoutMs?: number;
|
|
49
|
+
});
|
|
50
|
+
/** Called once at daemon start: no install can be running yet, so every `installing` record is left over. */
|
|
51
|
+
recover(): string[];
|
|
52
|
+
/** True from the moment an install or retry is accepted until its cleanup has finished. */
|
|
53
|
+
running(id: string): boolean;
|
|
54
|
+
/** Called after a removal, so a finished job is not reported for an extension that is gone. */
|
|
55
|
+
forget(id: string): void;
|
|
56
|
+
start(request: InstallRequest): Promise<InstallJob>;
|
|
57
|
+
private begin;
|
|
58
|
+
/** Start again from the commit and folder on the record, for a failed, cancelled or interrupted install. */
|
|
59
|
+
retry(id: string): Promise<InstallJob>;
|
|
60
|
+
cancel(id: string): InstallJob;
|
|
61
|
+
status(id: string): InstallJob;
|
|
62
|
+
/** Resolves once the current install of `id` has stopped, whichever way. For tests and shutdown. */
|
|
63
|
+
settled(id: string): Promise<void>;
|
|
64
|
+
/** One install per id at a time; an installed copy is not replaced (updating is its own operation). */
|
|
65
|
+
private claim;
|
|
66
|
+
private snapshot;
|
|
67
|
+
private launch;
|
|
68
|
+
private run;
|
|
69
|
+
}
|
|
70
|
+
export {};
|