astrocode-workflow 0.1.59 → 0.2.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/state/db.d.ts +1 -1
- package/dist/state/db.js +62 -4
- package/dist/tools/injects.js +90 -26
- package/dist/tools/workflow.d.ts +1 -1
- package/dist/tools/workflow.js +106 -101
- package/dist/ui/inject.d.ts +7 -1
- package/dist/ui/inject.js +86 -38
- package/dist/workflow/state-machine.d.ts +25 -9
- package/dist/workflow/state-machine.js +97 -106
- package/package.json +1 -1
- package/src/state/db.ts +63 -4
- package/src/tools/injects.ts +147 -53
- package/src/tools/workflow.ts +147 -140
- package/src/ui/inject.ts +107 -40
- package/src/workflow/state-machine.ts +127 -137
package/src/tools/injects.ts
CHANGED
|
@@ -5,34 +5,51 @@ import { withTx } from "../state/db";
|
|
|
5
5
|
import { nowISO } from "../shared/time";
|
|
6
6
|
import { sha256Hex } from "../shared/hash";
|
|
7
7
|
|
|
8
|
-
const VALID_INJECT_TYPES = [
|
|
9
|
-
type InjectType = typeof VALID_INJECT_TYPES[number];
|
|
8
|
+
const VALID_INJECT_TYPES = ["note", "policy", "reminder", "debug"] as const;
|
|
9
|
+
type InjectType = (typeof VALID_INJECT_TYPES)[number];
|
|
10
10
|
|
|
11
11
|
function validateInjectType(type: string): InjectType {
|
|
12
12
|
if (!VALID_INJECT_TYPES.includes(type as InjectType)) {
|
|
13
|
-
throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(
|
|
13
|
+
throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(", ")}`);
|
|
14
14
|
}
|
|
15
15
|
return type as InjectType;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function validateTimestamp(timestamp: string | null): string | null {
|
|
18
|
+
function validateTimestamp(timestamp: string | null | undefined): string | null {
|
|
19
19
|
if (!timestamp) return null;
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// Strict ISO 8601 UTC with Z suffix, sortable as string.
|
|
22
22
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
|
|
23
23
|
if (!isoRegex.test(timestamp)) {
|
|
24
|
-
throw new Error(
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Invalid timestamp format. Expected ISO 8601 UTC with Z suffix (e.g., "2026-01-23T13:05:19.000Z"), got: "${timestamp}"`
|
|
26
|
+
);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (isNaN(parsed.getTime())) {
|
|
29
|
+
const parsed = Date.parse(timestamp);
|
|
30
|
+
if (!Number.isFinite(parsed)) {
|
|
30
31
|
throw new Error(`Invalid timestamp: "${timestamp}" does not represent a valid date`);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
return timestamp;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function parseJsonStringArray(name: string, raw: string): string[] {
|
|
38
|
+
let v: unknown;
|
|
39
|
+
try {
|
|
40
|
+
v = JSON.parse(raw);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
43
|
+
throw new Error(`${name} must be valid JSON. Parse error: ${msg}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
|
|
47
|
+
throw new Error(`${name} must be a JSON array of strings`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return v as string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
function newInjectId(): string {
|
|
37
54
|
return `inj_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
38
55
|
}
|
|
@@ -58,17 +75,36 @@ export function createAstroInjectPutTool(opts: { ctx: any; config: AstrocodeConf
|
|
|
58
75
|
const now = nowISO();
|
|
59
76
|
const sha = sha256Hex(body_md);
|
|
60
77
|
|
|
61
|
-
// Validate inputs
|
|
62
78
|
const validatedType = validateInjectType(type);
|
|
63
79
|
const validatedExpiresAt = validateTimestamp(expires_at);
|
|
64
80
|
|
|
81
|
+
// Ensure tags_json is at least valid JSON (we do not enforce schema here beyond validity).
|
|
82
|
+
try {
|
|
83
|
+
JSON.parse(tags_json);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
86
|
+
throw new Error(`tags_json must be valid JSON. Parse error: ${msg}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
return withTx(db, () => {
|
|
66
90
|
const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id) as any;
|
|
67
91
|
|
|
68
92
|
if (existing) {
|
|
69
|
-
// Use INSERT ... ON CONFLICT for atomic updates
|
|
70
93
|
db.prepare(`
|
|
71
|
-
INSERT INTO injects (
|
|
94
|
+
INSERT INTO injects (
|
|
95
|
+
inject_id,
|
|
96
|
+
type,
|
|
97
|
+
title,
|
|
98
|
+
body_md,
|
|
99
|
+
tags_json,
|
|
100
|
+
scope,
|
|
101
|
+
source,
|
|
102
|
+
priority,
|
|
103
|
+
expires_at,
|
|
104
|
+
sha256,
|
|
105
|
+
created_at,
|
|
106
|
+
updated_at
|
|
107
|
+
)
|
|
72
108
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
73
109
|
ON CONFLICT(inject_id) DO UPDATE SET
|
|
74
110
|
type=excluded.type,
|
|
@@ -81,7 +117,8 @@ export function createAstroInjectPutTool(opts: { ctx: any; config: AstrocodeConf
|
|
|
81
117
|
expires_at=excluded.expires_at,
|
|
82
118
|
sha256=excluded.sha256,
|
|
83
119
|
updated_at=excluded.updated_at
|
|
84
|
-
`).run(id,
|
|
120
|
+
`).run(id, validatedType, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
|
|
121
|
+
|
|
85
122
|
return `✅ Updated inject ${id}: ${title}`;
|
|
86
123
|
}
|
|
87
124
|
|
|
@@ -108,9 +145,27 @@ export function createAstroInjectListTool(opts: { ctx: any; config: AstrocodeCon
|
|
|
108
145
|
execute: async ({ scope, type, limit }) => {
|
|
109
146
|
const where: string[] = [];
|
|
110
147
|
const params: any[] = [];
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
148
|
+
|
|
149
|
+
if (scope) {
|
|
150
|
+
where.push("scope = ?");
|
|
151
|
+
params.push(scope);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (type) {
|
|
155
|
+
// Keep list tool permissive (debugging), but still prevents obvious garbage if used.
|
|
156
|
+
validateInjectType(type);
|
|
157
|
+
where.push("type = ?");
|
|
158
|
+
params.push(type);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const sql = `
|
|
162
|
+
SELECT inject_id, type, title, scope, priority, created_at, updated_at
|
|
163
|
+
FROM injects
|
|
164
|
+
${where.length ? "WHERE " + where.join(" AND ") : ""}
|
|
165
|
+
ORDER BY priority DESC, updated_at DESC
|
|
166
|
+
LIMIT ?
|
|
167
|
+
`;
|
|
168
|
+
|
|
114
169
|
const rows = db.prepare(sql).all(...params, limit) as any[];
|
|
115
170
|
return JSON.stringify(rows, null, 2);
|
|
116
171
|
},
|
|
@@ -147,8 +202,20 @@ export function createAstroInjectSearchTool(opts: { ctx: any; config: AstrocodeC
|
|
|
147
202
|
const like = `%${q}%`;
|
|
148
203
|
const where: string[] = ["(title LIKE ? OR body_md LIKE ? OR tags_json LIKE ?)"];
|
|
149
204
|
const params: any[] = [like, like, like];
|
|
150
|
-
|
|
151
|
-
|
|
205
|
+
|
|
206
|
+
if (scope) {
|
|
207
|
+
where.push("scope = ?");
|
|
208
|
+
params.push(scope);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sql = `
|
|
212
|
+
SELECT inject_id, type, title, scope, priority, updated_at
|
|
213
|
+
FROM injects
|
|
214
|
+
WHERE ${where.join(" AND ")}
|
|
215
|
+
ORDER BY priority DESC, updated_at DESC
|
|
216
|
+
LIMIT ?
|
|
217
|
+
`;
|
|
218
|
+
|
|
152
219
|
const rows = db.prepare(sql).all(...params, limit) as any[];
|
|
153
220
|
return JSON.stringify(rows, null, 2);
|
|
154
221
|
},
|
|
@@ -170,15 +237,25 @@ export type InjectRow = {
|
|
|
170
237
|
updated_at: string;
|
|
171
238
|
};
|
|
172
239
|
|
|
173
|
-
export function selectEligibleInjects(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
240
|
+
export function selectEligibleInjects(
|
|
241
|
+
db: SqliteDb,
|
|
242
|
+
opts: {
|
|
243
|
+
nowIso: string;
|
|
244
|
+
scopeAllowlist: string[];
|
|
245
|
+
typeAllowlist: string[];
|
|
246
|
+
limit?: number;
|
|
247
|
+
}
|
|
248
|
+
): InjectRow[] {
|
|
179
249
|
const { nowIso, scopeAllowlist, typeAllowlist, limit = 50 } = opts;
|
|
180
250
|
|
|
181
|
-
|
|
251
|
+
if (!Array.isArray(scopeAllowlist) || scopeAllowlist.length === 0) {
|
|
252
|
+
throw new Error("selectEligibleInjects: scopeAllowlist must be a non-empty array");
|
|
253
|
+
}
|
|
254
|
+
if (!Array.isArray(typeAllowlist) || typeAllowlist.length === 0) {
|
|
255
|
+
throw new Error("selectEligibleInjects: typeAllowlist must be a non-empty array");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Build placeholders safely (guaranteed non-empty).
|
|
182
259
|
const scopeQs = scopeAllowlist.map(() => "?").join(", ");
|
|
183
260
|
const typeQs = typeAllowlist.map(() => "?").join(", ");
|
|
184
261
|
|
|
@@ -208,8 +285,11 @@ export function createAstroInjectEligibleTool(opts: { ctx: any; config: Astrocod
|
|
|
208
285
|
},
|
|
209
286
|
execute: async ({ scopes_json, types_json, limit }) => {
|
|
210
287
|
const now = nowISO();
|
|
211
|
-
const scopes =
|
|
212
|
-
const types =
|
|
288
|
+
const scopes = parseJsonStringArray("scopes_json", scopes_json);
|
|
289
|
+
const types = parseJsonStringArray("types_json", types_json);
|
|
290
|
+
|
|
291
|
+
// Validate types against the known set to keep selection sane.
|
|
292
|
+
for (const t of types) validateInjectType(t);
|
|
213
293
|
|
|
214
294
|
const rows = selectEligibleInjects(db, {
|
|
215
295
|
nowIso: now,
|
|
@@ -234,10 +314,13 @@ export function createAstroInjectDebugDueTool(opts: { ctx: any; config: Astrocod
|
|
|
234
314
|
},
|
|
235
315
|
execute: async ({ scopes_json, types_json }) => {
|
|
236
316
|
const now = nowISO();
|
|
237
|
-
const
|
|
238
|
-
|
|
317
|
+
const nowMs = Date.parse(now);
|
|
318
|
+
|
|
319
|
+
const scopes = parseJsonStringArray("scopes_json", scopes_json);
|
|
320
|
+
const types = parseJsonStringArray("types_json", types_json);
|
|
321
|
+
|
|
322
|
+
for (const t of types) validateInjectType(t);
|
|
239
323
|
|
|
240
|
-
// Get ALL injects to analyze filtering
|
|
241
324
|
const allInjects = db.prepare("SELECT * FROM injects").all() as any[];
|
|
242
325
|
|
|
243
326
|
let total = allInjects.length;
|
|
@@ -245,25 +328,31 @@ export function createAstroInjectDebugDueTool(opts: { ctx: any; config: Astrocod
|
|
|
245
328
|
let skippedExpired = 0;
|
|
246
329
|
let skippedScope = 0;
|
|
247
330
|
let skippedType = 0;
|
|
331
|
+
let skippedUnparseableExpiresAt = 0;
|
|
332
|
+
|
|
248
333
|
const excludedReasons: any[] = [];
|
|
249
334
|
const selectedInjects: any[] = [];
|
|
250
335
|
|
|
251
336
|
for (const inject of allInjects) {
|
|
252
337
|
const reasons: string[] = [];
|
|
253
338
|
|
|
254
|
-
//
|
|
255
|
-
if (inject.expires_at
|
|
256
|
-
|
|
257
|
-
|
|
339
|
+
// Expiration: parse to ms for correctness across legacy rows.
|
|
340
|
+
if (inject.expires_at) {
|
|
341
|
+
const expMs = Date.parse(String(inject.expires_at));
|
|
342
|
+
if (!Number.isFinite(expMs)) {
|
|
343
|
+
reasons.push("expires_at_unparseable");
|
|
344
|
+
skippedUnparseableExpiresAt++;
|
|
345
|
+
} else if (expMs <= nowMs) {
|
|
346
|
+
reasons.push("expired");
|
|
347
|
+
skippedExpired++;
|
|
348
|
+
}
|
|
258
349
|
}
|
|
259
350
|
|
|
260
|
-
// Check scope
|
|
261
351
|
if (!scopes.includes(inject.scope)) {
|
|
262
352
|
reasons.push("scope");
|
|
263
353
|
skippedScope++;
|
|
264
354
|
}
|
|
265
355
|
|
|
266
|
-
// Check type
|
|
267
356
|
if (!types.includes(inject.type)) {
|
|
268
357
|
reasons.push("type");
|
|
269
358
|
skippedType++;
|
|
@@ -273,7 +362,7 @@ export function createAstroInjectDebugDueTool(opts: { ctx: any; config: Astrocod
|
|
|
273
362
|
excludedReasons.push({
|
|
274
363
|
inject_id: inject.inject_id,
|
|
275
364
|
title: inject.title,
|
|
276
|
-
reasons
|
|
365
|
+
reasons,
|
|
277
366
|
scope: inject.scope,
|
|
278
367
|
type: inject.type,
|
|
279
368
|
expires_at: inject.expires_at,
|
|
@@ -290,23 +379,28 @@ export function createAstroInjectDebugDueTool(opts: { ctx: any; config: Astrocod
|
|
|
290
379
|
}
|
|
291
380
|
}
|
|
292
381
|
|
|
293
|
-
return JSON.stringify(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
382
|
+
return JSON.stringify(
|
|
383
|
+
{
|
|
384
|
+
now,
|
|
385
|
+
scopes_considered: scopes,
|
|
386
|
+
types_considered: types,
|
|
387
|
+
summary: {
|
|
388
|
+
total_injects: total,
|
|
389
|
+
selected_eligible: selected,
|
|
390
|
+
excluded_total: total - selected,
|
|
391
|
+
skipped_breakdown: {
|
|
392
|
+
expired: skippedExpired,
|
|
393
|
+
expires_at_unparseable: skippedUnparseableExpiresAt,
|
|
394
|
+
scope: skippedScope,
|
|
395
|
+
type: skippedType,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
selected_injects: selectedInjects,
|
|
399
|
+
excluded_injects: excludedReasons,
|
|
306
400
|
},
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
401
|
+
null,
|
|
402
|
+
2
|
|
403
|
+
);
|
|
310
404
|
},
|
|
311
405
|
});
|
|
312
406
|
}
|