@oqtopus-team/pi-qdash 0.1.3 → 0.1.5
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 +2 -1
- package/extensions/lib/forum-images.ts +82 -0
- package/extensions/lib/investigation-context.ts +169 -0
- package/extensions/lib/timeseries-analysis.ts +609 -0
- package/extensions/lib/write-gate.ts +1 -0
- package/extensions/qdash.ts +326 -4
- package/extensions/tools/timeseries.ts +211 -0
- package/package.json +4 -3
- package/skills/qdash/SKILL.md +34 -6
- package/skills/qdash-timeseries-diagnosis/SKILL.md +150 -0
package/extensions/qdash.ts
CHANGED
|
@@ -10,13 +10,16 @@ import { Type } from "typebox";
|
|
|
10
10
|
|
|
11
11
|
import { analyzePlotlyFigure, figureAnalysisText } from "./lib/figure-analysis.js";
|
|
12
12
|
import { fetchFigureDetails, figureComponent, figureResultText, mediaTypeForPath, type FigureDetails } from "./lib/figures.js";
|
|
13
|
+
import { forumImageReplyContent, forumImageReplyPreview, loadLocalForumImages, type LocalForumImage } from "./lib/forum-images.js";
|
|
13
14
|
import { forumDetailComponent, forumDetailLines, forumListLines, forumPostTitle, forumPostsFromPayload } from "./lib/forum-render.js";
|
|
15
|
+
import { investigationContextSummary, validateInvestigationContext, type TimeseriesInvestigationContext } from "./lib/investigation-context.js";
|
|
14
16
|
import { qdashObjectLinks, qdashWebBaseUrl, qdashWebUrl, safeConfig, withQDashLinks } from "./lib/links.js";
|
|
15
17
|
import { arrayFromPayload, compactItems, firstNumber, firstString, formatItem, payloadTotal, statusIcon } from "./lib/payload.js";
|
|
16
18
|
import { toTextToolResult, toToolResult } from "./lib/results.js";
|
|
17
19
|
import { ansi, boxLinesToWidth, boxed, formatNumber, textComponent } from "./lib/render.js";
|
|
18
20
|
import { plotSeriesLines, timeseriesPlotComponent, timeseriesPoints } from "./lib/timeseries-plot.js";
|
|
19
21
|
import { installQDashWriteGate } from "./lib/write-gate.js";
|
|
22
|
+
import { registerTimeseriesComparisonTool } from "./tools/timeseries.js";
|
|
20
23
|
import { analyzeWiringMarkdown, wiringInsightLines, type WiringInsights } from "./lib/wiring-analysis.js";
|
|
21
24
|
|
|
22
25
|
type QDashQueryParams = {
|
|
@@ -131,6 +134,7 @@ type QDashContextState = {
|
|
|
131
134
|
taskName?: string;
|
|
132
135
|
lastExecutionId?: string;
|
|
133
136
|
lastTaskId?: string;
|
|
137
|
+
investigation?: TimeseriesInvestigationContext;
|
|
134
138
|
};
|
|
135
139
|
|
|
136
140
|
const CONTEXT_ENTRY_TYPE = "qdash-context";
|
|
@@ -151,7 +155,8 @@ function contextSummary(): string {
|
|
|
151
155
|
const chip = currentContext.chipId ?? "auto-chip";
|
|
152
156
|
const session = currentContext.agentSessionId ? ` session:${shortId(currentContext.agentSessionId)}` : "";
|
|
153
157
|
const target = currentContext.qid ? ` q${currentContext.qid}` : currentContext.couplingId ? ` c${currentContext.couplingId}` : "";
|
|
154
|
-
|
|
158
|
+
const investigation = currentContext.investigation?.name ? ` investigation:${currentContext.investigation.name}` : "";
|
|
159
|
+
return `qdash ${profile} ${chip}${target}${session}${investigation}`;
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
function contextStatusLine(theme?: Theme): string {
|
|
@@ -179,8 +184,16 @@ function contextStatusLine(theme?: Theme): string {
|
|
|
179
184
|
function isQDashContextState(value: unknown): value is QDashContextState {
|
|
180
185
|
if (!value || typeof value !== "object") return false;
|
|
181
186
|
const context = value as QDashContextState;
|
|
182
|
-
|
|
187
|
+
const stringsValid = [context.profile, context.chipId, context.agentSessionId, context.qid, context.couplingId, context.taskName, context.lastExecutionId, context.lastTaskId]
|
|
183
188
|
.every((item) => item === undefined || typeof item === "string");
|
|
189
|
+
if (!stringsValid) return false;
|
|
190
|
+
if (context.investigation === undefined) return true;
|
|
191
|
+
try {
|
|
192
|
+
validateInvestigationContext(context.investigation);
|
|
193
|
+
return true;
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
184
197
|
}
|
|
185
198
|
|
|
186
199
|
function loadGlobalContext(): QDashContextState {
|
|
@@ -194,8 +207,9 @@ function loadGlobalContext(): QDashContextState {
|
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
function saveGlobalContext(context: QDashContextState): void {
|
|
210
|
+
const { investigation: _investigation, ...globalContext } = context;
|
|
197
211
|
mkdirSync(dirname(GLOBAL_CONTEXT_PATH), { recursive: true });
|
|
198
|
-
writeFileSync(GLOBAL_CONTEXT_PATH, `${JSON.stringify(
|
|
212
|
+
writeFileSync(GLOBAL_CONTEXT_PATH, `${JSON.stringify(globalContext, null, 2)}\n`, "utf8");
|
|
199
213
|
}
|
|
200
214
|
|
|
201
215
|
function adoptContextFromToolInput(input: unknown): boolean {
|
|
@@ -393,6 +407,58 @@ type TaskResultFigureFile = DownloadedFile & {
|
|
|
393
407
|
jsonFigurePaths: string[];
|
|
394
408
|
};
|
|
395
409
|
|
|
410
|
+
type CalibrationEvidence = {
|
|
411
|
+
id?: string;
|
|
412
|
+
source: string;
|
|
413
|
+
target?: Record<string, unknown>;
|
|
414
|
+
task?: Record<string, unknown>;
|
|
415
|
+
metrics?: Record<string, number | string | boolean | null>;
|
|
416
|
+
parameters?: Record<string, number | string | boolean | null>;
|
|
417
|
+
resultJson?: unknown;
|
|
418
|
+
figures?: Array<Record<string, unknown>>;
|
|
419
|
+
logs?: string[];
|
|
420
|
+
notes?: string;
|
|
421
|
+
history?: Array<Record<string, unknown>>;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
function parameterValueRecord(value: unknown): Record<string, number | string | boolean | null> {
|
|
425
|
+
const output: Record<string, number | string | boolean | null> = {};
|
|
426
|
+
const input = value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
427
|
+
for (const [key, raw] of Object.entries(input)) {
|
|
428
|
+
const object = raw && typeof raw === "object" ? raw as Record<string, unknown> : undefined;
|
|
429
|
+
const candidate = object && "value" in object ? object.value : raw;
|
|
430
|
+
if (typeof candidate === "number" || typeof candidate === "string" || typeof candidate === "boolean" || candidate === null) output[key] = candidate;
|
|
431
|
+
}
|
|
432
|
+
return output;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function firstStringField(object: Record<string, unknown>, keys: string[]): string | undefined {
|
|
436
|
+
for (const key of keys) {
|
|
437
|
+
const value = object[key];
|
|
438
|
+
if (typeof value === "string" && value) return value;
|
|
439
|
+
}
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function qcalEvidenceText(evidence: CalibrationEvidence): string {
|
|
444
|
+
const metrics = Object.entries(evidence.metrics ?? {}).slice(0, 8).map(([key, value]) => ` - ${key}: ${String(value)}`);
|
|
445
|
+
const figures = evidence.figures ?? [];
|
|
446
|
+
return boxed("QDash QCal Evidence", [
|
|
447
|
+
`id ${evidence.id ?? "n/a"}`,
|
|
448
|
+
`source ${evidence.source}`,
|
|
449
|
+
`target ${evidence.target?.id ?? evidence.target?.couplingId ?? evidence.target?.qids ?? "n/a"}`,
|
|
450
|
+
`task ${evidence.task?.name ?? "n/a"} (${evidence.task?.status ?? "unknown"})`,
|
|
451
|
+
"",
|
|
452
|
+
"Metrics",
|
|
453
|
+
...(metrics.length ? metrics : [" - none"]),
|
|
454
|
+
"",
|
|
455
|
+
`Figures ${figures.length}`,
|
|
456
|
+
...figures.map((figure, index) => ` - #${index + 1} ${String(figure.role ?? "plot")} ${String(figure.path ?? figure.url ?? "")}`),
|
|
457
|
+
"",
|
|
458
|
+
"Use qcal_evaluate_bundle with details.evidence to evaluate this evidence with pi-qcal.",
|
|
459
|
+
]).join("\n");
|
|
460
|
+
}
|
|
461
|
+
|
|
396
462
|
function cleanQuery(values: Record<string, unknown> = {}): Record<string, unknown> {
|
|
397
463
|
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined && value !== null));
|
|
398
464
|
}
|
|
@@ -1288,6 +1354,56 @@ function taskFigurePaths(task: Record<string, unknown>, maxFigures: number): str
|
|
|
1288
1354
|
return Array.isArray(paths) ? paths.filter((item): item is string => typeof item === "string").slice(0, maxFigures) : [];
|
|
1289
1355
|
}
|
|
1290
1356
|
|
|
1357
|
+
type ForumImageReplyParams = {
|
|
1358
|
+
parentPostId: string;
|
|
1359
|
+
imagePaths: string[];
|
|
1360
|
+
interpretation: string;
|
|
1361
|
+
title?: string;
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
type PreparedForumImageReply = {
|
|
1365
|
+
parent: Record<string, unknown>;
|
|
1366
|
+
images: LocalForumImage[];
|
|
1367
|
+
title: string;
|
|
1368
|
+
preview: string;
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
async function prepareForumImageReply(client: QDashClient, params: ForumImageReplyParams): Promise<PreparedForumImageReply> {
|
|
1372
|
+
const parent = await client.getForumPost(params.parentPostId) as unknown as Record<string, unknown>;
|
|
1373
|
+
const images = loadLocalForumImages(params.imagePaths);
|
|
1374
|
+
const title = params.title ?? `${new Date().toISOString().slice(0, 10)} 追加分析画像`;
|
|
1375
|
+
const preview = forumImageReplyPreview(title, images, params.interpretation);
|
|
1376
|
+
return { parent, images, title, preview };
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
async function createForumImageReply(client: QDashClient, params: ForumImageReplyParams, prepared: PreparedForumImageReply) {
|
|
1380
|
+
const uploaded = [];
|
|
1381
|
+
for (const image of prepared.images) {
|
|
1382
|
+
const result = await client.uploadForumImage(image.blob, image.filename);
|
|
1383
|
+
uploaded.push({ ...image, url: result.url });
|
|
1384
|
+
}
|
|
1385
|
+
const content = forumImageReplyContent(prepared.title, uploaded, params.interpretation);
|
|
1386
|
+
const parent = prepared.parent;
|
|
1387
|
+
const request = {
|
|
1388
|
+
category: firstString(parent, ["category"]) ?? "other",
|
|
1389
|
+
title: null,
|
|
1390
|
+
content,
|
|
1391
|
+
content_blocks: [],
|
|
1392
|
+
parent_id: params.parentPostId,
|
|
1393
|
+
chip_id: firstString(parent, ["chip_id", "chipId"]),
|
|
1394
|
+
target_type: firstString(parent, ["target_type", "targetType"]),
|
|
1395
|
+
target_id: firstString(parent, ["target_id", "targetId"]),
|
|
1396
|
+
cooldown_id: firstString(parent, ["cooldown_id", "cooldownId"]),
|
|
1397
|
+
status: firstString(parent, ["status"]) ?? "open",
|
|
1398
|
+
};
|
|
1399
|
+
const reply = await client.createForumPost(request as never);
|
|
1400
|
+
return {
|
|
1401
|
+
reply,
|
|
1402
|
+
content,
|
|
1403
|
+
images: uploaded.map(({ blob: _blob, ...image }) => image),
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1291
1407
|
async function buildForumEvidenceReply(client: QDashClient, params: ForumEvidenceReplyParams) {
|
|
1292
1408
|
const [parent, task] = await Promise.all([
|
|
1293
1409
|
client.getForumPost(params.parentPostId) as unknown as Promise<Record<string, unknown>>,
|
|
@@ -1676,6 +1792,12 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
1676
1792
|
},
|
|
1677
1793
|
});
|
|
1678
1794
|
|
|
1795
|
+
registerTimeseriesComparisonTool(pi, {
|
|
1796
|
+
makeClient,
|
|
1797
|
+
defaultChipId,
|
|
1798
|
+
investigationContext: () => currentContext.investigation,
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1679
1801
|
registerQueryTool({
|
|
1680
1802
|
name: "qdash_list_task_results",
|
|
1681
1803
|
label: "QDash List Task Results",
|
|
@@ -2187,6 +2309,65 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
2187
2309
|
},
|
|
2188
2310
|
});
|
|
2189
2311
|
|
|
2312
|
+
pi.registerTool({
|
|
2313
|
+
name: "qdash_preview_forum_image_reply",
|
|
2314
|
+
label: "QDash Preview Forum Image Reply",
|
|
2315
|
+
description: "Preview a QDash forum reply that will upload and embed locally generated analysis images. Read-only; no image or post is created.",
|
|
2316
|
+
promptSnippet: "Preview local analysis images and forum reply content before uploading",
|
|
2317
|
+
promptGuidelines: [
|
|
2318
|
+
"Use qdash_preview_forum_image_reply before uploading locally generated analysis images to a forum thread.",
|
|
2319
|
+
"Show the image paths and reply content, then require explicit confirmation before publishing.",
|
|
2320
|
+
],
|
|
2321
|
+
parameters: Type.Object({
|
|
2322
|
+
...connectionParams,
|
|
2323
|
+
parentPostId: Type.String(),
|
|
2324
|
+
imagePaths: Type.Array(Type.String(), { minItems: 1, maxItems: 8 }),
|
|
2325
|
+
interpretation: Type.String({ description: "Human-readable interpretation or context shown after the images." }),
|
|
2326
|
+
title: Type.Optional(Type.String()),
|
|
2327
|
+
}),
|
|
2328
|
+
async execute(_toolCallId, params: { profile?: string; configPath?: string; useEnv?: boolean; parentPostId: string; imagePaths: string[]; interpretation: string; title?: string }) {
|
|
2329
|
+
const client = await makeClient(params);
|
|
2330
|
+
const prepared = await prepareForumImageReply(client, params);
|
|
2331
|
+
return toTextToolResult(prepared.preview, {
|
|
2332
|
+
preview: prepared.preview,
|
|
2333
|
+
parent: prepared.parent,
|
|
2334
|
+
images: prepared.images.map(({ blob: _blob, ...image }) => image),
|
|
2335
|
+
}, { tool: "qdash_preview_forum_image_reply" });
|
|
2336
|
+
},
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
pi.registerTool({
|
|
2340
|
+
name: "qdash_create_forum_image_reply",
|
|
2341
|
+
label: "QDash Create Forum Image Reply",
|
|
2342
|
+
description: "Upload locally generated analysis images and create a QDash forum reply embedding them. This is a write operation and requires confirmation.",
|
|
2343
|
+
promptSnippet: "Upload local analysis images and create a forum reply after explicit confirmation",
|
|
2344
|
+
promptGuidelines: [
|
|
2345
|
+
"Use qdash_create_forum_image_reply only after previewing the exact image paths and reply content with qdash_preview_forum_image_reply.",
|
|
2346
|
+
"Require explicit confirmation before uploading images or creating the reply.",
|
|
2347
|
+
],
|
|
2348
|
+
parameters: Type.Object({
|
|
2349
|
+
...connectionParams,
|
|
2350
|
+
parentPostId: Type.String(),
|
|
2351
|
+
imagePaths: Type.Array(Type.String(), { minItems: 1, maxItems: 8 }),
|
|
2352
|
+
interpretation: Type.String({ description: "Human-readable interpretation or context shown after the images." }),
|
|
2353
|
+
title: Type.Optional(Type.String()),
|
|
2354
|
+
confirmWrite: Type.Optional(Type.Boolean()),
|
|
2355
|
+
}),
|
|
2356
|
+
async execute(_toolCallId, params: ConfirmableParams & { profile?: string; configPath?: string; useEnv?: boolean; parentPostId: string; imagePaths: string[]; interpretation: string; title?: string }, _signal, _onUpdate, ctx) {
|
|
2357
|
+
const client = await makeClient(params);
|
|
2358
|
+
const prepared = await prepareForumImageReply(client, params);
|
|
2359
|
+
if (!params.confirmWrite) {
|
|
2360
|
+
const confirmed = ctx.hasUI && await ctx.ui.confirm("Upload images and create QDash forum reply?", prepared.preview.slice(0, 1800));
|
|
2361
|
+
if (!confirmed) throw new Error("qdash_create_forum_image_reply requires explicit confirmation");
|
|
2362
|
+
}
|
|
2363
|
+
const created = await createForumImageReply(client, params, prepared);
|
|
2364
|
+
const postId = firstString(created.reply as unknown as Record<string, unknown>, ["id"]);
|
|
2365
|
+
const parentUrl = qdashWebUrl(client, `/forum/posts/${encodeURIComponent(params.parentPostId)}`);
|
|
2366
|
+
const text = [`created forum image reply${postId ? ` ${postId}` : ""}`, `parent ${parentUrl}`, "", created.content].join("\n");
|
|
2367
|
+
return toTextToolResult(text, created, { tool: "qdash_create_forum_image_reply", url: parentUrl });
|
|
2368
|
+
},
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2190
2371
|
pi.registerTool({
|
|
2191
2372
|
name: "qdash_create_forum_evidence_reply",
|
|
2192
2373
|
label: "QDash Create Forum Evidence Reply",
|
|
@@ -2462,6 +2643,105 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
2462
2643
|
},
|
|
2463
2644
|
});
|
|
2464
2645
|
|
|
2646
|
+
pi.registerTool({
|
|
2647
|
+
name: "qdash_build_qcal_evidence",
|
|
2648
|
+
label: "QDash Build QCal Evidence",
|
|
2649
|
+
description: "Build a provider-neutral CalibrationEvidence bundle from a QDash task result for evaluation by pi-qcal.",
|
|
2650
|
+
promptSnippet: "Convert QDash calibration task results into pi-qcal CalibrationEvidence",
|
|
2651
|
+
promptGuidelines: [
|
|
2652
|
+
"Use qdash_build_qcal_evidence before qcal_evaluate_bundle when the user asks to evaluate QDash calibration data with pi-qcal.",
|
|
2653
|
+
"qdash_build_qcal_evidence is read-only and only converts QDash data into a generic evidence bundle; it does not evaluate, execute, commit, or apply calibration parameters.",
|
|
2654
|
+
"After qdash_build_qcal_evidence, pass details.evidence to qcal_evaluate_bundle and treat the result as advisory evidence.",
|
|
2655
|
+
],
|
|
2656
|
+
parameters: Type.Object({
|
|
2657
|
+
...connectionParams,
|
|
2658
|
+
taskId: Type.String(),
|
|
2659
|
+
includeFigures: Type.Optional(Type.Boolean({ description: "Embed task image figures as data URLs. Defaults to true." })),
|
|
2660
|
+
includeFigureAnalysis: Type.Optional(Type.Boolean({ description: "Analyze Plotly JSON figures and include summaries/warnings in notes. Defaults to true." })),
|
|
2661
|
+
maxFigures: Type.Optional(Type.Number({ description: "Maximum image figures to embed. Defaults to 1." })),
|
|
2662
|
+
includeRawTaskResult: Type.Optional(Type.Boolean({ description: "Include the full raw task result in resultJson. Defaults to false." })),
|
|
2663
|
+
}),
|
|
2664
|
+
async execute(_toolCallId, params: { profile?: string; configPath?: string; useEnv?: boolean; taskId: string; includeFigures?: boolean; includeFigureAnalysis?: boolean; maxFigures?: number; includeRawTaskResult?: boolean }) {
|
|
2665
|
+
const client = await makeClient(params);
|
|
2666
|
+
const task = await client.getTaskResult(params.taskId) as unknown as Record<string, unknown>;
|
|
2667
|
+
const figures: Array<Record<string, unknown>> = [];
|
|
2668
|
+
const notes: string[] = [];
|
|
2669
|
+
const figurePaths = Array.isArray(task.figure_path) ? task.figure_path.filter((item): item is string => typeof item === "string") : [];
|
|
2670
|
+
const jsonFigurePaths = Array.isArray(task.json_figure_path) ? task.json_figure_path.filter((item): item is string => typeof item === "string") : [];
|
|
2671
|
+
|
|
2672
|
+
if (params.includeFigureAnalysis !== false) {
|
|
2673
|
+
const limit = Math.min(jsonFigurePaths.length, params.maxFigures ?? 1);
|
|
2674
|
+
for (let index = 0; index < limit; index += 1) {
|
|
2675
|
+
try {
|
|
2676
|
+
const file: TaskResultFigureFile = await client.getTaskResultFigure(params.taskId, { index, preferJson: true });
|
|
2677
|
+
const figure = JSON.parse(Buffer.from(file.data).toString("utf8")) as unknown;
|
|
2678
|
+
const analysis = analyzePlotlyFigure(file.path ?? jsonFigurePaths[index] ?? "", figure, params.taskId);
|
|
2679
|
+
notes.push(`Figure JSON analysis #${index + 1}:`, ...analysis.summaries.map((line) => `- ${line}`));
|
|
2680
|
+
if (analysis.warnings.length) notes.push("Warnings:", ...analysis.warnings.map((line) => `- ${line}`));
|
|
2681
|
+
figures.push({ path: file.path ?? jsonFigurePaths[index], role: "plotly", data: figure });
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
notes.push(`Figure JSON analysis #${index + 1} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
if (params.includeFigures !== false) {
|
|
2689
|
+
const limit = Math.min(figurePaths.length, params.maxFigures ?? 1);
|
|
2690
|
+
for (let index = 0; index < limit; index += 1) {
|
|
2691
|
+
try {
|
|
2692
|
+
const file: TaskResultFigureFile = await client.getTaskResultFigure(params.taskId, { index, preferJson: false });
|
|
2693
|
+
const bytes = Buffer.from(file.data);
|
|
2694
|
+
const mediaType = file.mediaType || mediaTypeForPath(file.path ?? figurePaths[index] ?? "");
|
|
2695
|
+
if (mediaType.startsWith("image/")) {
|
|
2696
|
+
figures.push({ path: file.path ?? figurePaths[index], url: `data:${mediaType};base64,${bytes.toString("base64")}`, mimeType: mediaType, role: "plot" });
|
|
2697
|
+
}
|
|
2698
|
+
} catch (error) {
|
|
2699
|
+
notes.push(`Figure image #${index + 1} embedding failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
const couplingId = firstStringField(task, ["coupling_id", "couplingId", "qid"]);
|
|
2705
|
+
const qid = firstStringField(task, ["qid"]);
|
|
2706
|
+
const evidence: CalibrationEvidence = {
|
|
2707
|
+
id: params.taskId,
|
|
2708
|
+
source: "external",
|
|
2709
|
+
target: {
|
|
2710
|
+
kind: couplingId && couplingId.includes("-") ? "coupling" : qid ? "qubit" : "system",
|
|
2711
|
+
id: couplingId ?? qid,
|
|
2712
|
+
couplingId: couplingId && couplingId.includes("-") ? couplingId : undefined,
|
|
2713
|
+
qids: qid && !qid.includes("-") ? [qid] : undefined,
|
|
2714
|
+
chipId: firstStringField(task, ["chip_id", "chipId"]),
|
|
2715
|
+
},
|
|
2716
|
+
task: {
|
|
2717
|
+
name: firstStringField(task, ["task_name", "taskName", "name"]),
|
|
2718
|
+
objective: `Evaluate QDash calibration task ${firstStringField(task, ["task_name", "taskName", "name"]) ?? params.taskId}`,
|
|
2719
|
+
backend: firstStringField(task, ["backend", "chip_id", "chipId"]),
|
|
2720
|
+
startedAt: firstStringField(task, ["start_at", "startAt"]),
|
|
2721
|
+
completedAt: firstStringField(task, ["end_at", "endAt"]),
|
|
2722
|
+
status: firstStringField(task, ["status"]),
|
|
2723
|
+
},
|
|
2724
|
+
metrics: parameterValueRecord(task.output_parameters),
|
|
2725
|
+
parameters: { ...parameterValueRecord(task.input_parameters), ...parameterValueRecord(task.run_parameters) },
|
|
2726
|
+
figures: figures.length ? figures : undefined,
|
|
2727
|
+
notes: [
|
|
2728
|
+
typeof task.message === "string" ? `Task message: ${task.message}` : undefined,
|
|
2729
|
+
notes.length ? notes.join("\n") : undefined,
|
|
2730
|
+
].filter(Boolean).join("\n\n") || undefined,
|
|
2731
|
+
resultJson: params.includeRawTaskResult ? task : {
|
|
2732
|
+
task_id: task.task_id,
|
|
2733
|
+
execution_id: task.execution_id,
|
|
2734
|
+
flow_name: task.flow_name,
|
|
2735
|
+
figure_path: figurePaths,
|
|
2736
|
+
json_figure_path: jsonFigurePaths,
|
|
2737
|
+
links: task._links,
|
|
2738
|
+
},
|
|
2739
|
+
};
|
|
2740
|
+
|
|
2741
|
+
return toTextToolResult(qcalEvidenceText(evidence), { evidence }, { tool: "qdash_build_qcal_evidence", taskId: params.taskId, evidence });
|
|
2742
|
+
},
|
|
2743
|
+
});
|
|
2744
|
+
|
|
2465
2745
|
registerQueryTool({
|
|
2466
2746
|
name: "qdash_get_provenance_stats",
|
|
2467
2747
|
label: "QDash Provenance Stats",
|
|
@@ -2916,8 +3196,49 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
2916
3196
|
},
|
|
2917
3197
|
});
|
|
2918
3198
|
|
|
3199
|
+
pi.registerCommand("qdash-investigation-setup", {
|
|
3200
|
+
description: "Create or edit a reusable generic timeseries investigation context; accepts optional JSON",
|
|
3201
|
+
handler: async (args, ctx) => {
|
|
3202
|
+
const template: TimeseriesInvestigationContext = currentContext.investigation ?? {
|
|
3203
|
+
name: "timeseries-investigation",
|
|
3204
|
+
timezoneOffsetMinutes: 540,
|
|
3205
|
+
qdashSeries: currentContext.qid ? [
|
|
3206
|
+
{ parameter: "metric_a", qid: currentContext.qid },
|
|
3207
|
+
{ parameter: "metric_b", qid: currentContext.qid },
|
|
3208
|
+
] : [
|
|
3209
|
+
{ parameter: "metric_a" },
|
|
3210
|
+
{ parameter: "metric_b" },
|
|
3211
|
+
],
|
|
3212
|
+
analysis: { resampleMinutes: 1, detrend: "none", normalize: "none" },
|
|
3213
|
+
};
|
|
3214
|
+
const source = args.trim() || await ctx.ui.editor("QDash investigation context (JSON)", JSON.stringify(template, null, 2));
|
|
3215
|
+
if (!source) return;
|
|
3216
|
+
try {
|
|
3217
|
+
const investigation = validateInvestigationContext(JSON.parse(source));
|
|
3218
|
+
currentContext = { ...currentContext, investigation };
|
|
3219
|
+
persistContext();
|
|
3220
|
+
refreshContextUi(ctx);
|
|
3221
|
+
ctx.ui.setWidget("qdash", ["QDash Investigation", investigationContextSummary(investigation)]);
|
|
3222
|
+
ctx.ui.notify(`Investigation context saved: ${investigationContextSummary(investigation)}`, "info");
|
|
3223
|
+
} catch (error) {
|
|
3224
|
+
ctx.ui.notify(`Invalid investigation context: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3225
|
+
}
|
|
3226
|
+
},
|
|
3227
|
+
});
|
|
3228
|
+
|
|
3229
|
+
pi.registerCommand("qdash-clear-investigation", {
|
|
3230
|
+
description: "Clear the reusable timeseries investigation context without changing profile/chip context",
|
|
3231
|
+
handler: async (_args, ctx) => {
|
|
3232
|
+
const { investigation: _investigation, ...rest } = currentContext;
|
|
3233
|
+
currentContext = rest;
|
|
3234
|
+
persistContext();
|
|
3235
|
+
refreshContextUi(ctx);
|
|
3236
|
+
ctx.ui.notify("QDash investigation context cleared", "info");
|
|
3237
|
+
},
|
|
3238
|
+
});
|
|
3239
|
+
|
|
2919
3240
|
pi.registerCommand("qdash-clear-context", {
|
|
2920
|
-
description: "Clear the current QDash profile/chip/agent-session context",
|
|
3241
|
+
description: "Clear the current QDash profile/chip/agent-session/investigation context",
|
|
2921
3242
|
handler: async (_args, ctx) => {
|
|
2922
3243
|
currentContext = {};
|
|
2923
3244
|
persistContext();
|
|
@@ -3029,6 +3350,7 @@ export default function qdashExtension(pi: ExtensionAPI) {
|
|
|
3029
3350
|
`last execution ${currentContext.lastExecutionId ?? "none"}`,
|
|
3030
3351
|
`last task ${currentContext.lastTaskId ?? "none"}`,
|
|
3031
3352
|
`agent session ${currentContext.agentSessionId ?? "none"}`,
|
|
3353
|
+
`investigation ${currentContext.investigation ? investigationContextSummary(currentContext.investigation) : "none"}`,
|
|
3032
3354
|
]);
|
|
3033
3355
|
ctx.ui.notify(contextSummary(), "info");
|
|
3034
3356
|
},
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { QDashClient } from "@oqtopus-team/qdash-client";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
compareTimeseries,
|
|
7
|
+
csvInspectionText,
|
|
8
|
+
inspectExternalCsv,
|
|
9
|
+
loadExternalCsvSeries,
|
|
10
|
+
timeseriesComparisonText,
|
|
11
|
+
type CsvTimeFormat,
|
|
12
|
+
type ExternalCsvSpec,
|
|
13
|
+
type NumericTimeseries,
|
|
14
|
+
} from "../lib/timeseries-analysis.js";
|
|
15
|
+
import type { TimeseriesInvestigationContext } from "../lib/investigation-context.js";
|
|
16
|
+
import { toTextToolResult } from "../lib/results.js";
|
|
17
|
+
import { timeseriesPoints } from "../lib/timeseries-plot.js";
|
|
18
|
+
|
|
19
|
+
type ComparisonToolParams = {
|
|
20
|
+
profile?: string;
|
|
21
|
+
configPath?: string;
|
|
22
|
+
useEnv?: boolean;
|
|
23
|
+
chipId?: string;
|
|
24
|
+
startAt?: string;
|
|
25
|
+
endAt?: string;
|
|
26
|
+
withinHours?: number;
|
|
27
|
+
qdashSeries?: Array<{ parameter: string; qid?: string; tag?: string; label?: string; unit?: string; scale?: number; offset?: number }>;
|
|
28
|
+
csvSeries?: Array<ExternalCsvSpec & { timeFormat?: CsvTimeFormat }>;
|
|
29
|
+
resampleMinutes?: number;
|
|
30
|
+
smoothingWindowMinutes?: number;
|
|
31
|
+
detrend?: string;
|
|
32
|
+
normalize?: string;
|
|
33
|
+
maxInterpolationGapMinutes?: number;
|
|
34
|
+
periodSearch?: { minMinutes: number; maxMinutes: number; stepMinutes?: number };
|
|
35
|
+
includeAlignedData?: boolean;
|
|
36
|
+
useInvestigationContext?: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type TimeseriesToolDependencies = {
|
|
40
|
+
makeClient: (params: { profile?: string; configPath?: string; useEnv?: boolean }) => Promise<QDashClient>;
|
|
41
|
+
defaultChipId: (client: QDashClient, chipId?: string) => Promise<string>;
|
|
42
|
+
investigationContext?: () => TimeseriesInvestigationContext | undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function registerTimeseriesComparisonTool(pi: ExtensionAPI, dependencies: TimeseriesToolDependencies): void {
|
|
46
|
+
pi.registerTool({
|
|
47
|
+
name: "qdash_inspect_timeseries_csv",
|
|
48
|
+
label: "QDash Inspect Timeseries CSV",
|
|
49
|
+
description: "Inspect a local CSV schema and numeric/time characteristics before mapping it into a QDash timeseries comparison. Read-only; never uploads the file.",
|
|
50
|
+
promptSnippet: "Inspect local CSV columns, numeric ranges, timestamps, and cadence before comparison",
|
|
51
|
+
promptGuidelines: [
|
|
52
|
+
"Use qdash_inspect_timeseries_csv before qdash_compare_timeseries when a local CSV schema, timestamp format, timezone, or numeric columns are not already known.",
|
|
53
|
+
"Do not infer the timezone of naive timestamps from qdash_inspect_timeseries_csv samples; ask for or supply an explicit timezone offset.",
|
|
54
|
+
],
|
|
55
|
+
parameters: Type.Object({
|
|
56
|
+
path: Type.String({ description: "Local CSV path, absolute or relative to the current working directory." }),
|
|
57
|
+
delimiter: Type.Optional(Type.String({ description: "Single-character delimiter. Defaults to comma." })),
|
|
58
|
+
skipRows: Type.Optional(Type.Number({ description: "Rows before the header. Defaults to 0." })),
|
|
59
|
+
timeColumn: Type.Optional(Type.String({ description: "Optional timestamp column to validate." })),
|
|
60
|
+
timeFormat: Type.Optional(Type.String({ description: "iso, yyyy/M/d H:mm, yyyy-MM-dd HH:mm:ss, unix-seconds, or unix-milliseconds." })),
|
|
61
|
+
timezoneOffsetMinutes: Type.Optional(Type.Number({ description: "Required to validate timestamps without an explicit zone; JST is 540." })),
|
|
62
|
+
scanRows: Type.Optional(Type.Number({ description: "Rows to profile, from 1 to 100000. Defaults to 10000." })),
|
|
63
|
+
}),
|
|
64
|
+
async execute(_toolCallId, params: {
|
|
65
|
+
path: string;
|
|
66
|
+
delimiter?: string;
|
|
67
|
+
skipRows?: number;
|
|
68
|
+
timeColumn?: string;
|
|
69
|
+
timeFormat?: CsvTimeFormat;
|
|
70
|
+
timezoneOffsetMinutes?: number;
|
|
71
|
+
scanRows?: number;
|
|
72
|
+
}, _signal, _onUpdate, ctx) {
|
|
73
|
+
const inspection = inspectExternalCsv(params, ctx.cwd);
|
|
74
|
+
return toTextToolResult(csvInspectionText(inspection), inspection, {
|
|
75
|
+
tool: "qdash_inspect_timeseries_csv",
|
|
76
|
+
path: inspection.path,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
pi.registerTool({
|
|
82
|
+
name: "qdash_compare_timeseries",
|
|
83
|
+
label: "QDash Compare Timeseries",
|
|
84
|
+
description: "Compare two or more QDash and/or local CSV numeric timeseries on a common time grid. Read-only; reports alignment, correlations, optional shared-period fits, phases, and statistical caveats without inferring causality.",
|
|
85
|
+
promptSnippet: "Compare generic QDash metrics and external CSV sensor timeseries",
|
|
86
|
+
promptGuidelines: [
|
|
87
|
+
"Use qdash_compare_timeseries when comparing multiple QDash metrics, targets, or local CSV sensor logs over time.",
|
|
88
|
+
"For qdash_compare_timeseries local CSV inputs, explicitly specify timestamp format and timezone offset when timestamps have no zone.",
|
|
89
|
+
"Treat qdash_compare_timeseries correlations and shared periods as evidence of association, not proof of causality.",
|
|
90
|
+
],
|
|
91
|
+
parameters: Type.Object({
|
|
92
|
+
profile: Type.Optional(Type.String({ description: "QDash profile name. Defaults to env when QDASH_BASE_URL is set, otherwise 'default'." })),
|
|
93
|
+
configPath: Type.Optional(Type.String({ description: "Optional path to qdash config.ini." })),
|
|
94
|
+
useEnv: Type.Optional(Type.Boolean({ description: "Force QDASH_* environment variables instead of a profile." })),
|
|
95
|
+
chipId: Type.Optional(Type.String({ description: "Chip ID. Defaults to the active/default chip when omitted." })),
|
|
96
|
+
startAt: Type.Optional(Type.String({ description: "Optional common start timestamp as an ISO timestamp with timezone." })),
|
|
97
|
+
endAt: Type.Optional(Type.String({ description: "Optional common end timestamp as an ISO timestamp with timezone." })),
|
|
98
|
+
withinHours: Type.Optional(Type.Number({ description: "QDash lookback when startAt is omitted. Defaults to 168 hours." })),
|
|
99
|
+
useInvestigationContext: Type.Optional(Type.Boolean({ description: "Use missing series/window/transform defaults from the active investigation context. Defaults to true." })),
|
|
100
|
+
qdashSeries: Type.Optional(Type.Array(Type.Object({
|
|
101
|
+
parameter: Type.String({ description: "QDash task-result parameter name." }),
|
|
102
|
+
qid: Type.Optional(Type.String()),
|
|
103
|
+
tag: Type.Optional(Type.String()),
|
|
104
|
+
label: Type.Optional(Type.String({ description: "Unique display label. Defaults to parameter plus qid/returned series." })),
|
|
105
|
+
unit: Type.Optional(Type.String({ description: "Optional output unit override." })),
|
|
106
|
+
scale: Type.Optional(Type.Number({ description: "Multiply QDash values by this factor. Defaults to 1." })),
|
|
107
|
+
offset: Type.Optional(Type.Number({ description: "Add this value after scaling. Defaults to 0." })),
|
|
108
|
+
}), { maxItems: 12 })),
|
|
109
|
+
csvSeries: Type.Optional(Type.Array(Type.Object({
|
|
110
|
+
path: Type.String({ description: "Local CSV path, absolute or relative to the current working directory." }),
|
|
111
|
+
timeColumn: Type.String(),
|
|
112
|
+
valueColumns: Type.Array(Type.Object({
|
|
113
|
+
column: Type.String(),
|
|
114
|
+
label: Type.Optional(Type.String()),
|
|
115
|
+
unit: Type.Optional(Type.String()),
|
|
116
|
+
scale: Type.Optional(Type.Number()),
|
|
117
|
+
offset: Type.Optional(Type.Number()),
|
|
118
|
+
}), { minItems: 1, maxItems: 12 }),
|
|
119
|
+
timeFormat: Type.Optional(Type.String({ description: "iso, yyyy/M/d H:mm, yyyy-MM-dd HH:mm:ss, unix-seconds, or unix-milliseconds. Defaults to iso." })),
|
|
120
|
+
timezoneOffsetMinutes: Type.Optional(Type.Number({ description: "Required for timestamps without an explicit zone; for JST use 540." })),
|
|
121
|
+
delimiter: Type.Optional(Type.String({ description: "Single-character delimiter. Defaults to comma." })),
|
|
122
|
+
skipRows: Type.Optional(Type.Number({ description: "Rows to skip before the header. Defaults to 0." })),
|
|
123
|
+
filters: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()), { description: "Optional exact-match row filters keyed by column name." })),
|
|
124
|
+
}), { maxItems: 8 })),
|
|
125
|
+
resampleMinutes: Type.Optional(Type.Number({ description: "Common grid interval. Defaults to 1 minute." })),
|
|
126
|
+
smoothingWindowMinutes: Type.Optional(Type.Number({ description: "Centered moving-average window. Zero disables smoothing." })),
|
|
127
|
+
detrend: Type.Optional(Type.String({ description: "none or linear. Defaults to none." })),
|
|
128
|
+
normalize: Type.Optional(Type.String({ description: "none or zscore. Defaults to none." })),
|
|
129
|
+
maxInterpolationGapMinutes: Type.Optional(Type.Number({ description: "Do not interpolate across larger source gaps." })),
|
|
130
|
+
periodSearch: Type.Optional(Type.Object({
|
|
131
|
+
minMinutes: Type.Number(),
|
|
132
|
+
maxMinutes: Type.Number(),
|
|
133
|
+
stepMinutes: Type.Optional(Type.Number()),
|
|
134
|
+
})),
|
|
135
|
+
includeAlignedData: Type.Optional(Type.Boolean({ description: "Include transformed aligned rows in structured details. Defaults to false." })),
|
|
136
|
+
}),
|
|
137
|
+
async execute(_toolCallId, params: ComparisonToolParams, _signal, _onUpdate, ctx) {
|
|
138
|
+
const investigation = params.useInvestigationContext === false ? undefined : dependencies.investigationContext?.();
|
|
139
|
+
const analysis = investigation?.analysis;
|
|
140
|
+
const detrend = params.detrend ?? analysis?.detrend ?? "none";
|
|
141
|
+
const normalize = params.normalize ?? analysis?.normalize ?? "none";
|
|
142
|
+
if (detrend !== "none" && detrend !== "linear") throw new Error("detrend must be 'none' or 'linear'");
|
|
143
|
+
if (normalize !== "none" && normalize !== "zscore") throw new Error("normalize must be 'none' or 'zscore'");
|
|
144
|
+
const csvSpecs = (params.csvSeries ?? investigation?.csvSeries ?? []).map((spec) => ({
|
|
145
|
+
...spec,
|
|
146
|
+
timezoneOffsetMinutes: spec.timezoneOffsetMinutes ?? investigation?.timezoneOffsetMinutes,
|
|
147
|
+
}));
|
|
148
|
+
const qdashSpecs = (params.qdashSeries ?? investigation?.qdashSeries ?? []).map((spec) => ({
|
|
149
|
+
...spec,
|
|
150
|
+
tag: spec.tag ?? investigation?.tag,
|
|
151
|
+
}));
|
|
152
|
+
const csv = csvSpecs.flatMap((spec) => loadExternalCsvSeries(spec, ctx.cwd));
|
|
153
|
+
const qdash: NumericTimeseries[] = [];
|
|
154
|
+
let resolvedStartAt = params.startAt ?? investigation?.startAt;
|
|
155
|
+
let resolvedEndAt = params.endAt ?? investigation?.endAt;
|
|
156
|
+
if (qdashSpecs.length > 0) {
|
|
157
|
+
resolvedEndAt = resolvedEndAt ?? new Date().toISOString();
|
|
158
|
+
resolvedStartAt = resolvedStartAt ?? new Date(Date.parse(resolvedEndAt) - (params.withinHours ?? 168) * 3_600_000).toISOString();
|
|
159
|
+
const client = await dependencies.makeClient(params);
|
|
160
|
+
const chipId = await dependencies.defaultChipId(client, params.chipId);
|
|
161
|
+
const fetched = await Promise.all(qdashSpecs.map(async (spec) => {
|
|
162
|
+
const data = await client.getTaskResultsTimeseries({
|
|
163
|
+
chipId,
|
|
164
|
+
parameter: spec.parameter,
|
|
165
|
+
qid: spec.qid,
|
|
166
|
+
tag: spec.tag,
|
|
167
|
+
startAt: resolvedStartAt!,
|
|
168
|
+
endAt: resolvedEndAt!,
|
|
169
|
+
});
|
|
170
|
+
return { spec, points: timeseriesPoints(data) };
|
|
171
|
+
}));
|
|
172
|
+
for (const { spec, points } of fetched) {
|
|
173
|
+
const groups = new Map<string, typeof points>();
|
|
174
|
+
for (const point of points) {
|
|
175
|
+
if (!point.at) continue;
|
|
176
|
+
const group = spec.qid ?? point.series;
|
|
177
|
+
groups.set(group, [...(groups.get(group) ?? []), point]);
|
|
178
|
+
}
|
|
179
|
+
for (const [group, values] of groups) {
|
|
180
|
+
const suffix = spec.qid ? ` q${spec.qid}` : groups.size > 1 ? ` ${group}` : "";
|
|
181
|
+
qdash.push({
|
|
182
|
+
label: spec.label ? groups.size > 1 ? `${spec.label} ${group}` : spec.label : `${spec.parameter}${suffix}`,
|
|
183
|
+
unit: spec.unit ?? values.find((point) => point.unit)?.unit,
|
|
184
|
+
source: "qdash",
|
|
185
|
+
points: values.map((point) => ({
|
|
186
|
+
at: point.at!,
|
|
187
|
+
value: point.value * (spec.scale ?? 1) + (spec.offset ?? 0),
|
|
188
|
+
})),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const comparison = compareTimeseries([...qdash, ...csv], {
|
|
194
|
+
startAt: resolvedStartAt,
|
|
195
|
+
endAt: resolvedEndAt,
|
|
196
|
+
resampleMinutes: params.resampleMinutes ?? analysis?.resampleMinutes,
|
|
197
|
+
smoothingWindowMinutes: params.smoothingWindowMinutes ?? analysis?.smoothingWindowMinutes,
|
|
198
|
+
detrend,
|
|
199
|
+
normalize,
|
|
200
|
+
maxInterpolationGapMinutes: params.maxInterpolationGapMinutes ?? analysis?.maxInterpolationGapMinutes,
|
|
201
|
+
periodSearch: params.periodSearch ?? analysis?.periodSearch,
|
|
202
|
+
includeAlignedData: params.includeAlignedData,
|
|
203
|
+
});
|
|
204
|
+
return toTextToolResult(timeseriesComparisonText(comparison), comparison, {
|
|
205
|
+
tool: "qdash_compare_timeseries",
|
|
206
|
+
seriesCount: comparison.series.length,
|
|
207
|
+
investigation: investigation?.name,
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oqtopus-team/pi-qdash",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Pi extension for querying QDash via @oqtopus-team/qdash-client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@oqtopus-team/qdash-client": "^0.1.
|
|
27
|
+
"@oqtopus-team/qdash-client": "^0.1.4"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
]
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"check": "tsc --noEmit"
|
|
42
|
+
"check": "tsc --noEmit",
|
|
43
|
+
"test": "rm -rf .test-dist && tsc --noEmit false --outDir .test-dist && node --test tests/*.test.mjs; status=$?; rm -rf .test-dist; exit $status"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|