@noir-ai/daemon 1.0.0-beta.1 → 1.2.0-beta.1
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/index.d.ts +235 -1
- package/dist/index.js +626 -16
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/dist/index.js
CHANGED
|
@@ -1,3 +1,372 @@
|
|
|
1
|
+
// src/clickup-write.ts
|
|
2
|
+
var CLICKUP_BASE = "https://api.clickup.com/api/v2";
|
|
3
|
+
var ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
4
|
+
function assertId(value, field) {
|
|
5
|
+
if (typeof value !== "string" || !ID_PATTERN.test(value)) {
|
|
6
|
+
throw new InvalidOp(`invalid ${field}: ${JSON.stringify(value)} (must match ${ID_PATTERN})`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
var InvalidOp = class extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "InvalidOp";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var OP_ALIASES = {
|
|
17
|
+
"task:set-status": "status",
|
|
18
|
+
"task:create-subtask": "subtask",
|
|
19
|
+
"task:comment": "comment",
|
|
20
|
+
"task:batch-create": "batch"
|
|
21
|
+
};
|
|
22
|
+
function normalizeOp(op) {
|
|
23
|
+
const resolved = OP_ALIASES[op] ?? op;
|
|
24
|
+
if (resolved !== "status" && resolved !== "subtask" && resolved !== "comment" && resolved !== "batch") {
|
|
25
|
+
throw new InvalidOp(`unknown op: ${JSON.stringify(op)}`);
|
|
26
|
+
}
|
|
27
|
+
return resolved;
|
|
28
|
+
}
|
|
29
|
+
function resolveBinding(payload, binding) {
|
|
30
|
+
const listId = (typeof payload.listId === "string" ? payload.listId : void 0) ?? binding.listId;
|
|
31
|
+
const teamId = (typeof payload.teamId === "string" ? payload.teamId : void 0) ?? binding.teamId;
|
|
32
|
+
const resolved = {};
|
|
33
|
+
if (listId !== void 0) resolved.listId = assertId(listId, "listId");
|
|
34
|
+
if (teamId !== void 0) resolved.teamId = assertId(teamId, "teamId");
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
function buildStatus(payload) {
|
|
38
|
+
const taskId = assertId(String(payload.taskId ?? ""), "taskId");
|
|
39
|
+
const status = payload.status;
|
|
40
|
+
if (typeof status !== "string" || status.length === 0) {
|
|
41
|
+
throw new InvalidOp("status op requires payload.status (non-empty string)");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
method: "PUT",
|
|
45
|
+
url: `${CLICKUP_BASE}/task/${taskId}`,
|
|
46
|
+
body: { status },
|
|
47
|
+
target: `task/${taskId}`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function buildSubtask(payload, binding) {
|
|
51
|
+
const resolved = resolveBinding(payload, binding);
|
|
52
|
+
if (!resolved.listId)
|
|
53
|
+
throw new InvalidOp(
|
|
54
|
+
"subtask op requires a listId (payload.listId or integrations.clickup.listId)"
|
|
55
|
+
);
|
|
56
|
+
const parentTaskId = assertId(String(payload.parentTaskId ?? ""), "parentTaskId");
|
|
57
|
+
const name = payload.name;
|
|
58
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
59
|
+
throw new InvalidOp("subtask op requires payload.name (non-empty string)");
|
|
60
|
+
}
|
|
61
|
+
const create = {
|
|
62
|
+
method: "POST",
|
|
63
|
+
url: `${CLICKUP_BASE}/list/${resolved.listId}/task`,
|
|
64
|
+
body: { name, parent: parentTaskId },
|
|
65
|
+
target: `list/${resolved.listId}/task (parent ${parentTaskId})`
|
|
66
|
+
};
|
|
67
|
+
const reqs = [create];
|
|
68
|
+
const status = typeof payload.status === "string" && payload.status.length > 0 ? payload.status : void 0;
|
|
69
|
+
if (status !== void 0) {
|
|
70
|
+
reqs.push({
|
|
71
|
+
method: "PUT",
|
|
72
|
+
url: `${CLICKUP_BASE}/task/__sub_id__`,
|
|
73
|
+
body: { status },
|
|
74
|
+
target: `task/<new subtask> (status)`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return reqs;
|
|
78
|
+
}
|
|
79
|
+
function buildComment(payload) {
|
|
80
|
+
const taskId = assertId(String(payload.taskId ?? ""), "taskId");
|
|
81
|
+
const commentText = payload.commentText;
|
|
82
|
+
if (typeof commentText !== "string" || commentText.length === 0) {
|
|
83
|
+
throw new InvalidOp("comment op requires payload.commentText (non-empty string)");
|
|
84
|
+
}
|
|
85
|
+
const notifyAll = typeof payload.notifyAll === "boolean" ? payload.notifyAll : false;
|
|
86
|
+
const body = { comment_text: commentText, notify_all: notifyAll };
|
|
87
|
+
if (payload.assigneeId !== void 0 && payload.assigneeId !== null) {
|
|
88
|
+
const assignee = typeof payload.assigneeId === "number" ? payload.assigneeId : Number(payload.assigneeId);
|
|
89
|
+
if (!Number.isFinite(assignee))
|
|
90
|
+
throw new InvalidOp("comment op payload.assigneeId must be a number");
|
|
91
|
+
body.assignee = assignee;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
method: "POST",
|
|
95
|
+
url: `${CLICKUP_BASE}/task/${taskId}/comment`,
|
|
96
|
+
body,
|
|
97
|
+
target: `task/${taskId}/comment`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function parseH2Tasks(markdown) {
|
|
101
|
+
const lines = markdown.split(/\r?\n/);
|
|
102
|
+
const tasks = [];
|
|
103
|
+
let cur = null;
|
|
104
|
+
const desc = [];
|
|
105
|
+
for (const raw of lines) {
|
|
106
|
+
const line = raw;
|
|
107
|
+
const h2 = /^##\s+(.+?)\s*$/.exec(line);
|
|
108
|
+
if (h2) {
|
|
109
|
+
const title = h2[1];
|
|
110
|
+
if (title !== void 0) {
|
|
111
|
+
if (cur) cur.description = desc.join("\n").trim() || void 0;
|
|
112
|
+
cur = { name: title.trim() };
|
|
113
|
+
desc.length = 0;
|
|
114
|
+
tasks.push(cur);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!cur) {
|
|
119
|
+
if (line.trim().length === 0) continue;
|
|
120
|
+
cur = { name: line.trim() };
|
|
121
|
+
desc.length = 0;
|
|
122
|
+
tasks.push(cur);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const meta = /^-\s+([A-Za-z_]+)\s*:\s*(.+?)\s*$/.exec(line);
|
|
126
|
+
if (meta) {
|
|
127
|
+
const keyRaw = meta[1];
|
|
128
|
+
const valRaw = meta[2];
|
|
129
|
+
if (keyRaw !== void 0 && valRaw !== void 0) {
|
|
130
|
+
const key = keyRaw.toLowerCase();
|
|
131
|
+
const val = valRaw;
|
|
132
|
+
if (key === "tag" || key === "tags") {
|
|
133
|
+
cur.tags = [
|
|
134
|
+
...cur.tags ?? [],
|
|
135
|
+
...val.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
|
|
136
|
+
];
|
|
137
|
+
} else if (key === "assignee" || key === "assignees") {
|
|
138
|
+
const ids = val.split(",").map((t) => t.trim()).map((t) => Number(t)).filter((n) => Number.isFinite(n));
|
|
139
|
+
cur.assignees = [...cur.assignees ?? [], ...ids];
|
|
140
|
+
} else if (key === "status") {
|
|
141
|
+
cur.status = val;
|
|
142
|
+
} else {
|
|
143
|
+
desc.push(line);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
desc.push(line);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
desc.push(line);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (cur) cur.description = desc.join("\n").trim() || void 0;
|
|
153
|
+
return tasks.filter((t) => t.name.length > 0);
|
|
154
|
+
}
|
|
155
|
+
function normalizeBatchInput(payload) {
|
|
156
|
+
if (Array.isArray(payload.tasks)) {
|
|
157
|
+
return payload.tasks.map((t, i) => {
|
|
158
|
+
if (typeof t !== "object" || t === null)
|
|
159
|
+
throw new InvalidOp(`batch payload.tasks[${i}] must be an object`);
|
|
160
|
+
const obj = t;
|
|
161
|
+
const name = obj.name;
|
|
162
|
+
if (typeof name !== "string" || name.length === 0)
|
|
163
|
+
throw new InvalidOp(`batch payload.tasks[${i}].name required`);
|
|
164
|
+
const out = { name };
|
|
165
|
+
if (typeof obj.description === "string") out.description = obj.description;
|
|
166
|
+
if (Array.isArray(obj.tags)) out.tags = obj.tags.map((s) => String(s));
|
|
167
|
+
if (Array.isArray(obj.assignees))
|
|
168
|
+
out.assignees = obj.assignees.map((n) => Number(n)).filter((n) => Number.isFinite(n));
|
|
169
|
+
if (typeof obj.status === "string") out.status = obj.status;
|
|
170
|
+
return out;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (typeof payload.markdown === "string") {
|
|
174
|
+
return parseH2Tasks(payload.markdown);
|
|
175
|
+
}
|
|
176
|
+
throw new InvalidOp("batch op requires payload.tasks[] OR payload.markdown (H2-per-task)");
|
|
177
|
+
}
|
|
178
|
+
function buildBatch(payload, binding) {
|
|
179
|
+
const resolved = resolveBinding(payload, binding);
|
|
180
|
+
if (!resolved.listId)
|
|
181
|
+
throw new InvalidOp(
|
|
182
|
+
"batch op requires a listId (payload.listId or integrations.clickup.listId)"
|
|
183
|
+
);
|
|
184
|
+
const tasks = normalizeBatchInput(payload);
|
|
185
|
+
if (tasks.length === 0)
|
|
186
|
+
throw new InvalidOp("batch op yielded no tasks (empty markdown / tasks[])");
|
|
187
|
+
return tasks.map((t, i) => {
|
|
188
|
+
const body = { name: t.name };
|
|
189
|
+
if (t.description !== void 0) body.description = t.description;
|
|
190
|
+
if (t.tags !== void 0 && t.tags.length > 0) body.tags = t.tags;
|
|
191
|
+
if (t.assignees !== void 0 && t.assignees.length > 0) body.assignees = t.assignees;
|
|
192
|
+
return {
|
|
193
|
+
method: "POST",
|
|
194
|
+
url: `${CLICKUP_BASE}/list/${resolved.listId}/task`,
|
|
195
|
+
body,
|
|
196
|
+
target: `list/${resolved.listId}/task (#${i + 1} ${t.name.slice(0, 40)})`
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function buildRequests(opRaw, payload, binding) {
|
|
201
|
+
const op = normalizeOp(opRaw);
|
|
202
|
+
switch (op) {
|
|
203
|
+
case "status":
|
|
204
|
+
return { op, requests: [buildStatus(payload)] };
|
|
205
|
+
case "subtask":
|
|
206
|
+
return { op, requests: buildSubtask(payload, binding) };
|
|
207
|
+
case "comment":
|
|
208
|
+
return { op, requests: [buildComment(payload)] };
|
|
209
|
+
case "batch":
|
|
210
|
+
return { op, requests: buildBatch(payload, binding) };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function previewRows(requests) {
|
|
214
|
+
return requests.map((r) => ({
|
|
215
|
+
method: r.method,
|
|
216
|
+
url: r.url,
|
|
217
|
+
...r.body !== void 0 ? { body: r.body } : {},
|
|
218
|
+
auth: "pk_***",
|
|
219
|
+
target: r.target
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
function authHeaders(token) {
|
|
223
|
+
return {
|
|
224
|
+
"content-type": "application/json",
|
|
225
|
+
authorization: `pk_${token}`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function rateLimitWaitMs(res) {
|
|
229
|
+
const reset = res.headers.get("X-RateLimit-Reset");
|
|
230
|
+
if (reset === null) return void 0;
|
|
231
|
+
const epochSec = Number(reset);
|
|
232
|
+
if (!Number.isFinite(epochSec)) return void 0;
|
|
233
|
+
const waitMs = epochSec * 1e3 - Date.now();
|
|
234
|
+
if (waitMs <= 0) return 0;
|
|
235
|
+
if (waitMs > 6e4) return 6e4;
|
|
236
|
+
return waitMs;
|
|
237
|
+
}
|
|
238
|
+
async function sleep(ms) {
|
|
239
|
+
if (ms <= 0) return;
|
|
240
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
241
|
+
}
|
|
242
|
+
async function executeOne(req, token, fetchImpl) {
|
|
243
|
+
const init = {
|
|
244
|
+
method: req.method,
|
|
245
|
+
headers: authHeaders(token),
|
|
246
|
+
...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
|
|
247
|
+
};
|
|
248
|
+
let res;
|
|
249
|
+
try {
|
|
250
|
+
res = await fetchImpl(req.url, init);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
return {
|
|
253
|
+
target: req.target,
|
|
254
|
+
method: req.method,
|
|
255
|
+
url: req.url,
|
|
256
|
+
httpStatus: 0,
|
|
257
|
+
success: false,
|
|
258
|
+
response: {},
|
|
259
|
+
error: err instanceof Error ? err.message : String(err)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (res.status === 429) {
|
|
263
|
+
const waitMs = rateLimitWaitMs(res) ?? 1e3;
|
|
264
|
+
await sleep(waitMs);
|
|
265
|
+
try {
|
|
266
|
+
res = await fetchImpl(req.url, init);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
return {
|
|
269
|
+
target: req.target,
|
|
270
|
+
method: req.method,
|
|
271
|
+
url: req.url,
|
|
272
|
+
httpStatus: 429,
|
|
273
|
+
success: false,
|
|
274
|
+
response: {},
|
|
275
|
+
rateLimitedWaitMs: waitMs,
|
|
276
|
+
error: err instanceof Error ? err.message : String(err)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const final = await toExecResult(req, res);
|
|
280
|
+
return { ...final, rateLimitedWaitMs: waitMs };
|
|
281
|
+
}
|
|
282
|
+
return toExecResult(req, res);
|
|
283
|
+
}
|
|
284
|
+
async function toExecResult(req, res) {
|
|
285
|
+
const success = res.status >= 200 && res.status < 300;
|
|
286
|
+
let parsed = {};
|
|
287
|
+
try {
|
|
288
|
+
parsed = await res.json();
|
|
289
|
+
} catch {
|
|
290
|
+
try {
|
|
291
|
+
const text = await res.text();
|
|
292
|
+
parsed = text.length > 0 ? { _raw: text } : {};
|
|
293
|
+
} catch {
|
|
294
|
+
parsed = {};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const result = {
|
|
298
|
+
target: req.target,
|
|
299
|
+
method: req.method,
|
|
300
|
+
url: req.url,
|
|
301
|
+
httpStatus: res.status,
|
|
302
|
+
success,
|
|
303
|
+
response: parsed
|
|
304
|
+
};
|
|
305
|
+
if (!success) {
|
|
306
|
+
const errVal = parsed.err;
|
|
307
|
+
result.error = typeof errVal === "string" ? errVal : `HTTP ${res.status}`;
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
312
|
+
const results = new Array(items.length);
|
|
313
|
+
let cursor = 0;
|
|
314
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
315
|
+
while (true) {
|
|
316
|
+
const i = cursor++;
|
|
317
|
+
if (i >= items.length) break;
|
|
318
|
+
const item = items[i];
|
|
319
|
+
if (item !== void 0) results[i] = await fn(item, i);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
await Promise.all(workers);
|
|
323
|
+
return results;
|
|
324
|
+
}
|
|
325
|
+
async function executeOp(op, requests, token, fetchImpl) {
|
|
326
|
+
if (op === "subtask" && requests.length === 2) {
|
|
327
|
+
const create = requests[0];
|
|
328
|
+
const statusReq = requests[1];
|
|
329
|
+
if (!create || !statusReq) {
|
|
330
|
+
throw new InvalidOp("subtask op expects exactly two rendered requests");
|
|
331
|
+
}
|
|
332
|
+
const createRes = await executeOne(create, token, fetchImpl);
|
|
333
|
+
if (!createRes.success) return [createRes];
|
|
334
|
+
const newId = typeof createRes.response.id === "string" ? createRes.response.id : void 0;
|
|
335
|
+
if (!newId) {
|
|
336
|
+
return [
|
|
337
|
+
createRes,
|
|
338
|
+
{
|
|
339
|
+
target: statusReq.target,
|
|
340
|
+
method: statusReq.method,
|
|
341
|
+
url: statusReq.url,
|
|
342
|
+
httpStatus: 0,
|
|
343
|
+
success: false,
|
|
344
|
+
response: {},
|
|
345
|
+
error: "subtask created but no id returned; status follow-up skipped"
|
|
346
|
+
}
|
|
347
|
+
];
|
|
348
|
+
}
|
|
349
|
+
createRes.newTaskId = newId;
|
|
350
|
+
assertId(newId, "subtask-create-response.id");
|
|
351
|
+
const realStatusReq = {
|
|
352
|
+
method: statusReq.method,
|
|
353
|
+
url: statusReq.url.replace("__sub_id__", newId),
|
|
354
|
+
target: `task/${newId} (status)`,
|
|
355
|
+
...statusReq.body !== void 0 ? { body: statusReq.body } : {}
|
|
356
|
+
};
|
|
357
|
+
const statusRes = await executeOne(realStatusReq, token, fetchImpl);
|
|
358
|
+
return [createRes, statusRes];
|
|
359
|
+
}
|
|
360
|
+
if (op === "batch") {
|
|
361
|
+
return mapWithConcurrency(requests, 4, (req) => executeOne(req, token, fetchImpl));
|
|
362
|
+
}
|
|
363
|
+
const out = [];
|
|
364
|
+
for (const req of requests) {
|
|
365
|
+
out.push(await executeOne(req, token, fetchImpl));
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
|
|
1
370
|
// src/context-seam.ts
|
|
2
371
|
import { ContextEngine } from "@noir-ai/context";
|
|
3
372
|
function buildContextEngine(store, root, projectId, embedderCfg, storeDegraded) {
|
|
@@ -15,15 +384,84 @@ import { createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
|
|
|
15
384
|
import { resolveMemoryConfig } from "@noir-ai/memory";
|
|
16
385
|
import { resolveModelConfig } from "@noir-ai/model";
|
|
17
386
|
|
|
387
|
+
// src/integration-seam.ts
|
|
388
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
389
|
+
import { join } from "path";
|
|
390
|
+
import { paths } from "@noir-ai/core";
|
|
391
|
+
import { discoverIntegrations } from "@noir-ai/skills";
|
|
392
|
+
function shortNameOf(name) {
|
|
393
|
+
return name.replace(/^noir-/, "");
|
|
394
|
+
}
|
|
395
|
+
function buildIntegrationService(root, configIntegrations = {}) {
|
|
396
|
+
let discovered = [];
|
|
397
|
+
try {
|
|
398
|
+
discovered = discoverIntegrations().map(
|
|
399
|
+
(s) => s.declaration
|
|
400
|
+
);
|
|
401
|
+
} catch {
|
|
402
|
+
discovered = [];
|
|
403
|
+
}
|
|
404
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
405
|
+
for (const declaration of discovered) {
|
|
406
|
+
const cfg = configIntegrations[declaration.name] ?? configIntegrations[shortNameOf(declaration.name)];
|
|
407
|
+
const tokenEnv = cfg?.auth?.tokenEnv ?? declaration.auth.tokenEnv;
|
|
408
|
+
const effectiveRuntime = cfg?.runtime ?? declaration.runtime;
|
|
409
|
+
const binding = {
|
|
410
|
+
name: declaration.name,
|
|
411
|
+
shortName: shortNameOf(declaration.name),
|
|
412
|
+
declaration,
|
|
413
|
+
tokenEnv,
|
|
414
|
+
effectiveRuntime,
|
|
415
|
+
...cfg?.teamId !== void 0 ? { teamId: cfg.teamId } : {},
|
|
416
|
+
...cfg?.listId !== void 0 ? { listId: cfg.listId } : {},
|
|
417
|
+
...cfg?.spaceId !== void 0 ? { spaceId: cfg.spaceId } : {}
|
|
418
|
+
};
|
|
419
|
+
bindings.set(binding.name, binding);
|
|
420
|
+
bindings.set(binding.shortName, binding);
|
|
421
|
+
}
|
|
422
|
+
return { root, bindings };
|
|
423
|
+
}
|
|
424
|
+
function findBinding(svc, name) {
|
|
425
|
+
return svc.bindings.get(name);
|
|
426
|
+
}
|
|
427
|
+
function resolveToken(svc, opts, env = process.env) {
|
|
428
|
+
const integration = opts.integration;
|
|
429
|
+
const envVar = opts.envVar;
|
|
430
|
+
if (integration !== void 0) {
|
|
431
|
+
const binding = findBinding(svc, integration);
|
|
432
|
+
if (!binding) return { ok: false, reason: "unknown-integration", integration };
|
|
433
|
+
const name = binding.tokenEnv;
|
|
434
|
+
const value = env[name];
|
|
435
|
+
if (typeof value === "string" && value.length > 0) {
|
|
436
|
+
return { ok: true, token: value, envVar: name };
|
|
437
|
+
}
|
|
438
|
+
return { ok: false, reason: "no-token", envVar: name };
|
|
439
|
+
}
|
|
440
|
+
if (envVar !== void 0) {
|
|
441
|
+
const value = env[envVar];
|
|
442
|
+
if (typeof value === "string" && value.length > 0) {
|
|
443
|
+
return { ok: true, token: value, envVar };
|
|
444
|
+
}
|
|
445
|
+
return { ok: false, reason: "no-token", envVar };
|
|
446
|
+
}
|
|
447
|
+
return { ok: false, reason: "no-token", envVar: "" };
|
|
448
|
+
}
|
|
449
|
+
function writeIntegrationAudit(root, integrationName, entry) {
|
|
450
|
+
const file = join(paths.auditDir(root), `integration-${shortNameOf(integrationName)}.jsonl`);
|
|
451
|
+
mkdirSync(paths.auditDir(root), { recursive: true });
|
|
452
|
+
appendFileSync(file, `${JSON.stringify(entry)}
|
|
453
|
+
`, "utf8");
|
|
454
|
+
}
|
|
455
|
+
|
|
18
456
|
// src/lifecycle.ts
|
|
19
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
457
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync, rmSync, writeFileSync } from "fs";
|
|
20
458
|
import { homedir } from "os";
|
|
21
|
-
import { join } from "path";
|
|
459
|
+
import { join as join2 } from "path";
|
|
22
460
|
function noirHome() {
|
|
23
|
-
return
|
|
461
|
+
return join2(homedir(), ".noir");
|
|
24
462
|
}
|
|
25
463
|
function daemonJsonPath() {
|
|
26
|
-
return process.env.NOIR_DAEMON_JSON ??
|
|
464
|
+
return process.env.NOIR_DAEMON_JSON ?? join2(noirHome(), "daemon.json");
|
|
27
465
|
}
|
|
28
466
|
function readDaemonRecord() {
|
|
29
467
|
try {
|
|
@@ -36,7 +474,7 @@ function readDaemonRecord() {
|
|
|
36
474
|
}
|
|
37
475
|
}
|
|
38
476
|
function writeDaemonRecord(rec) {
|
|
39
|
-
|
|
477
|
+
mkdirSync2(noirHome(), { recursive: true });
|
|
40
478
|
writeFileSync(daemonJsonPath(), `${JSON.stringify(rec)}
|
|
41
479
|
`, "utf8");
|
|
42
480
|
}
|
|
@@ -62,7 +500,7 @@ function resolveMemoryConsolidation(modelCfg) {
|
|
|
62
500
|
const providerKey = modelCfg.tiers.consolidate ?? modelCfg.defaultProvider;
|
|
63
501
|
if (!providerKey) return null;
|
|
64
502
|
const block = modelCfg.providers[providerKey];
|
|
65
|
-
if (!block
|
|
503
|
+
if (!block?.model) return null;
|
|
66
504
|
return { provider: providerKey, model: block.model };
|
|
67
505
|
}
|
|
68
506
|
function resolveConsolidationCapability(resolvedMemory, modelCfg) {
|
|
@@ -128,7 +566,7 @@ import { z } from "zod";
|
|
|
128
566
|
|
|
129
567
|
// src/status.ts
|
|
130
568
|
import { NOIR_VERSION } from "@noir-ai/core";
|
|
131
|
-
function
|
|
569
|
+
function buildStatus2(project, ctx) {
|
|
132
570
|
const status = {
|
|
133
571
|
noir: NOIR_VERSION,
|
|
134
572
|
project: { id: project.id, name: project.name },
|
|
@@ -194,7 +632,7 @@ function createNoirServer(ctx) {
|
|
|
194
632
|
inputSchema: {}
|
|
195
633
|
},
|
|
196
634
|
async () => {
|
|
197
|
-
const status =
|
|
635
|
+
const status = buildStatus2(ctx.project, ctx);
|
|
198
636
|
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
199
637
|
}
|
|
200
638
|
);
|
|
@@ -369,7 +807,7 @@ function createNoirServer(ctx) {
|
|
|
369
807
|
paths: z.array(z.string()).optional().describe('Files/directories to index (repo-relative or absolute); defaults to ["."].')
|
|
370
808
|
}
|
|
371
809
|
},
|
|
372
|
-
async ({ paths:
|
|
810
|
+
async ({ paths: paths3 }) => {
|
|
373
811
|
if (storeDegraded) {
|
|
374
812
|
return textResult({
|
|
375
813
|
ok: false,
|
|
@@ -378,7 +816,7 @@ function createNoirServer(ctx) {
|
|
|
378
816
|
});
|
|
379
817
|
}
|
|
380
818
|
try {
|
|
381
|
-
const result = await context.indexPaths(
|
|
819
|
+
const result = await context.indexPaths(paths3 && paths3.length > 0 ? paths3 : ["."]);
|
|
382
820
|
return textResult({ ok: true, ...result });
|
|
383
821
|
} catch (err) {
|
|
384
822
|
return textResult({ ok: false, degraded: true, error: errorMessage(err) });
|
|
@@ -541,14 +979,172 @@ function createNoirServer(ctx) {
|
|
|
541
979
|
);
|
|
542
980
|
}
|
|
543
981
|
}
|
|
982
|
+
if (ctx.integrations) {
|
|
983
|
+
const integrations = ctx.integrations;
|
|
984
|
+
server.registerTool(
|
|
985
|
+
"integrations_auth",
|
|
986
|
+
{
|
|
987
|
+
description: "Resolve an integration token VALUE server-side at call time (kills the non-interactive-shell gotcha). Pass {integration:'noir-clickup'} to resolve tokenEnv from the discovered declaration, or {envVar:'CLICKUP_API_TOKEN'} to name the env var directly. Returns {ok:true,token,envVar} when present, or {ok:false,reason:'no-token',envVar} when absent (the skill then does manual-paste fallback). The token is returned ONLY in this tool result \u2014 never logged, never persisted.",
|
|
988
|
+
inputSchema: {
|
|
989
|
+
integration: z.string().optional().describe(
|
|
990
|
+
"Integration name (e.g. 'noir-clickup' or 'clickup'); resolves tokenEnv from the discovered declaration (config override honored)."
|
|
991
|
+
),
|
|
992
|
+
envVar: z.string().optional().describe(
|
|
993
|
+
"Env-var name to read directly (e.g. CLICKUP_API_TOKEN). Used when no integration binding applies."
|
|
994
|
+
)
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
async ({ integration, envVar }) => {
|
|
998
|
+
if (integration === void 0 && envVar === void 0) {
|
|
999
|
+
return textResult({
|
|
1000
|
+
ok: false,
|
|
1001
|
+
reason: "invalid-input",
|
|
1002
|
+
error: "pass one of {integration} or {envVar}"
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
const resolution = resolveToken(integrations, { integration, envVar });
|
|
1006
|
+
return textResult(resolution);
|
|
1007
|
+
}
|
|
1008
|
+
);
|
|
1009
|
+
const clickupBinding = findBinding(integrations, "noir-clickup");
|
|
1010
|
+
if (clickupBinding && clickupBinding.effectiveRuntime === "gated-write-proxy") {
|
|
1011
|
+
const binding = clickupBinding;
|
|
1012
|
+
const root = integrations.root;
|
|
1013
|
+
server.registerTool(
|
|
1014
|
+
"noir.clickup_write",
|
|
1015
|
+
{
|
|
1016
|
+
description: "ClickUp gated-write-proxy: renders a DRY-RUN preview of the exact HTTP request(s) for an op, and ONLY on explicit {confirm:true} executes them server-side with the pk_ token (NO Bearer). Ops: status (PUT /task/{id}), subtask (POST /list/{list_id}/task + optional PUT status), comment (POST /task/{id}/comment), batch (loop POST /list/{list_id}/task, concurrency 4, 429 backoff on X-RateLimit-Reset). URLs are allowlisted \u2014 a caller-supplied url is ignored (prompt-injection defense). Executed writes are audited to .noir/audit/.",
|
|
1017
|
+
inputSchema: {
|
|
1018
|
+
op: z.enum([
|
|
1019
|
+
"status",
|
|
1020
|
+
"subtask",
|
|
1021
|
+
"comment",
|
|
1022
|
+
"batch",
|
|
1023
|
+
// `task:`-prefixed aliases emitted by the LOCKED noir-clickup
|
|
1024
|
+
// SKILL.md (X-T1+T2); normalized to the short form internally.
|
|
1025
|
+
"task:set-status",
|
|
1026
|
+
"task:create-subtask",
|
|
1027
|
+
"task:comment",
|
|
1028
|
+
"task:batch-create"
|
|
1029
|
+
]).describe(
|
|
1030
|
+
"Write op. Short form ('status'|'subtask'|'comment'|'batch') or the task:-prefixed form the skill emits."
|
|
1031
|
+
),
|
|
1032
|
+
// Flat op-specific fields (matches the LOCKED skill's call shape;
|
|
1033
|
+
// the X-T3 `payload` nesting is honored by also accepting a payload
|
|
1034
|
+
// object — the handler reads both).
|
|
1035
|
+
taskId: z.string().optional().describe("status/comment: the ClickUp task id."),
|
|
1036
|
+
status: z.string().optional().describe("status: the new status value; subtask: optional new-subtask status."),
|
|
1037
|
+
parentTaskId: z.string().optional().describe("subtask: the parent task id (same list)."),
|
|
1038
|
+
name: z.string().optional().describe("subtask: the new subtask name."),
|
|
1039
|
+
listId: z.string().optional().describe("subtask/batch: overrides integrations.clickup.listId."),
|
|
1040
|
+
teamId: z.string().optional().describe("Optional team-id override (custom-id reads)."),
|
|
1041
|
+
commentText: z.string().optional().describe("comment: the comment body."),
|
|
1042
|
+
notifyAll: z.boolean().optional().describe("comment: notify_all flag (default false)."),
|
|
1043
|
+
assigneeId: z.number().optional().describe("comment: assignee user id (integer)."),
|
|
1044
|
+
tasks: z.array(z.record(z.string(), z.unknown())).optional().describe(
|
|
1045
|
+
"batch: normalized tasks array ({name,description?,tags?,assignees?,status?})."
|
|
1046
|
+
),
|
|
1047
|
+
markdown: z.string().optional().describe("batch: H2-per-task markdown (## Title \\n body \\n - tag: x)."),
|
|
1048
|
+
payload: z.record(z.string(), z.unknown()).optional().describe("Alternative: a single payload object with any of the fields above."),
|
|
1049
|
+
confirm: z.boolean().optional().describe(
|
|
1050
|
+
"HARD confirm gate. Unless true, the tool is DRY-RUN (no fetch). Default false."
|
|
1051
|
+
),
|
|
1052
|
+
dryRun: z.boolean().optional().describe("Force a dry-run preview even if confirm is true (safe default wins).")
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
async (input) => {
|
|
1056
|
+
const payloadRaw = {
|
|
1057
|
+
...input.payload ?? {},
|
|
1058
|
+
...Object.fromEntries(
|
|
1059
|
+
Object.entries(input).filter(
|
|
1060
|
+
([k, v]) => k !== "op" && k !== "confirm" && k !== "dryRun" && k !== "payload" && v !== void 0
|
|
1061
|
+
)
|
|
1062
|
+
)
|
|
1063
|
+
};
|
|
1064
|
+
let built;
|
|
1065
|
+
try {
|
|
1066
|
+
built = buildRequests(input.op, payloadRaw, {
|
|
1067
|
+
...binding.teamId !== void 0 ? { teamId: binding.teamId } : {},
|
|
1068
|
+
...binding.listId !== void 0 ? { listId: binding.listId } : {}
|
|
1069
|
+
});
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1072
|
+
return textResult({ ok: false, reason: "invalid-op", op: input.op, error: message });
|
|
1073
|
+
}
|
|
1074
|
+
const execute = input.confirm === true && input.dryRun !== true;
|
|
1075
|
+
if (!execute) {
|
|
1076
|
+
return textResult({
|
|
1077
|
+
ok: true,
|
|
1078
|
+
mode: "dry-run",
|
|
1079
|
+
op: built.op,
|
|
1080
|
+
integration: "noir-clickup",
|
|
1081
|
+
preview: previewRows(built.requests),
|
|
1082
|
+
note: "Pass {confirm:true} to execute. No network call was made."
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
const resolution = resolveToken(integrations, { integration: "noir-clickup" });
|
|
1086
|
+
if (!resolution.ok) {
|
|
1087
|
+
return textResult({
|
|
1088
|
+
ok: false,
|
|
1089
|
+
reason: resolution.reason,
|
|
1090
|
+
// `envVar` is present on the no-token branch; the unknown-integration
|
|
1091
|
+
// branch carries `integration` instead. Spread conditionally so the
|
|
1092
|
+
// union narrows cleanly.
|
|
1093
|
+
..."envVar" in resolution ? { envVar: resolution.envVar } : {},
|
|
1094
|
+
op: built.op,
|
|
1095
|
+
error: "no token resolved; set the env var or use the manual-paste fallback"
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
const token = resolution.token;
|
|
1099
|
+
const op = built.op;
|
|
1100
|
+
let results;
|
|
1101
|
+
try {
|
|
1102
|
+
results = await executeOp(op, built.requests, token, (url, init) => fetch(url, init));
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1105
|
+
return textResult({ ok: false, reason: "http-error", op, error: message });
|
|
1106
|
+
}
|
|
1107
|
+
let audited = true;
|
|
1108
|
+
for (const r of results) {
|
|
1109
|
+
const entry = {
|
|
1110
|
+
kind: "integration",
|
|
1111
|
+
integration: "noir-clickup",
|
|
1112
|
+
op: normalizeOp(input.op),
|
|
1113
|
+
target: r.target,
|
|
1114
|
+
method: r.method,
|
|
1115
|
+
httpStatus: r.httpStatus,
|
|
1116
|
+
success: r.success,
|
|
1117
|
+
timestamp: Date.now(),
|
|
1118
|
+
...r.rateLimitedWaitMs !== void 0 ? { rateLimitedWaitMs: r.rateLimitedWaitMs } : {},
|
|
1119
|
+
...r.error !== void 0 ? { error: r.error } : {}
|
|
1120
|
+
};
|
|
1121
|
+
try {
|
|
1122
|
+
writeIntegrationAudit(root, "noir-clickup", entry);
|
|
1123
|
+
} catch {
|
|
1124
|
+
audited = false;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const allOk = results.length > 0 && results.every((r) => r.success);
|
|
1128
|
+
return textResult({
|
|
1129
|
+
ok: allOk,
|
|
1130
|
+
mode: "executed",
|
|
1131
|
+
op,
|
|
1132
|
+
integration: "noir-clickup",
|
|
1133
|
+
results,
|
|
1134
|
+
audited
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
544
1140
|
return server;
|
|
545
1141
|
}
|
|
546
1142
|
|
|
547
1143
|
// src/store-seam.ts
|
|
548
|
-
import { paths } from "@noir-ai/core";
|
|
1144
|
+
import { paths as paths2 } from "@noir-ai/core";
|
|
549
1145
|
import { openStore } from "@noir-ai/store";
|
|
550
1146
|
async function openStoreForDaemon(projectId, root) {
|
|
551
|
-
const dbPath =
|
|
1147
|
+
const dbPath = paths2.storeDb(root, projectId);
|
|
552
1148
|
try {
|
|
553
1149
|
const store = await openStore({ projectId, root });
|
|
554
1150
|
return { store, dbPath, degraded: false };
|
|
@@ -599,6 +1195,7 @@ async function startHttpServer(opts) {
|
|
|
599
1195
|
daemonStore.degraded,
|
|
600
1196
|
resolvedMemory
|
|
601
1197
|
) : void 0;
|
|
1198
|
+
const integrations = buildIntegrationService(opts.project.root, opts.project.config.integrations);
|
|
602
1199
|
const httpServer = createServer(async (req, res) => {
|
|
603
1200
|
lastActivity = Date.now();
|
|
604
1201
|
if (req.method === "GET" && req.url === "/health") {
|
|
@@ -623,7 +1220,8 @@ async function startHttpServer(opts) {
|
|
|
623
1220
|
} : {},
|
|
624
1221
|
...engine ? { engine } : {},
|
|
625
1222
|
...context ? { context } : {},
|
|
626
|
-
...memory ? { memory, memoryConsolidation } : {}
|
|
1223
|
+
...memory ? { memory, memoryConsolidation } : {},
|
|
1224
|
+
...integrations ? { integrations } : {}
|
|
627
1225
|
});
|
|
628
1226
|
const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
629
1227
|
await server.connect(transport);
|
|
@@ -723,6 +1321,7 @@ async function startStdioServer(ctx) {
|
|
|
723
1321
|
daemonStore.degraded,
|
|
724
1322
|
resolvedMemory
|
|
725
1323
|
) : void 0;
|
|
1324
|
+
const integrations = buildIntegrationService(ctx.project.root, ctx.project.config.integrations);
|
|
726
1325
|
const server = createNoirServer({
|
|
727
1326
|
...ctx,
|
|
728
1327
|
...daemonStore ? {
|
|
@@ -732,28 +1331,39 @@ async function startStdioServer(ctx) {
|
|
|
732
1331
|
} : {},
|
|
733
1332
|
...engine ? { engine } : {},
|
|
734
1333
|
...context ? { context } : {},
|
|
735
|
-
...memory ? { memory, memoryConsolidation } : {}
|
|
1334
|
+
...memory ? { memory, memoryConsolidation } : {},
|
|
1335
|
+
...integrations ? { integrations } : {}
|
|
736
1336
|
});
|
|
737
1337
|
const transport = new StdioServerTransport();
|
|
738
1338
|
await server.connect(transport);
|
|
739
1339
|
}
|
|
740
1340
|
export {
|
|
1341
|
+
InvalidOp,
|
|
741
1342
|
buildContextEngine,
|
|
1343
|
+
buildIntegrationService,
|
|
742
1344
|
buildMemoryEngine,
|
|
743
|
-
|
|
1345
|
+
buildRequests,
|
|
1346
|
+
buildStatus2 as buildStatus,
|
|
744
1347
|
buildWorkflowEngine,
|
|
745
1348
|
clearDaemonRecord,
|
|
746
1349
|
createNoirServer,
|
|
747
1350
|
daemonJsonPath,
|
|
748
1351
|
ensureDaemonRunning,
|
|
1352
|
+
executeOp,
|
|
1353
|
+
findBinding,
|
|
749
1354
|
noirHome,
|
|
750
1355
|
openStoreForDaemon,
|
|
1356
|
+
parseH2Tasks,
|
|
751
1357
|
pidAlive,
|
|
1358
|
+
previewRows,
|
|
752
1359
|
readDaemonRecord,
|
|
753
1360
|
resolveConsolidationCapability,
|
|
754
1361
|
resolveMemoryConsolidation,
|
|
1362
|
+
resolveToken,
|
|
1363
|
+
shortNameOf,
|
|
755
1364
|
startHttpServer,
|
|
756
1365
|
startStdioServer,
|
|
757
|
-
writeDaemonRecord
|
|
1366
|
+
writeDaemonRecord,
|
|
1367
|
+
writeIntegrationAudit
|
|
758
1368
|
};
|
|
759
1369
|
//# sourceMappingURL=index.js.map
|