@wrongstack/core 0.299.0 → 0.301.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chronicle/index.d.ts +2 -0
- package/dist/chronicle/index.js +297 -10
- package/dist/chronicle/project-server.js +253 -15
- package/dist/chronicle/sqlite-compaction.d.ts +20 -0
- package/dist/chronicle/sqlite-journal.d.ts +57 -0
- package/dist/coordination/agents/capability-manifest.d.ts +7 -0
- package/dist/coordination/agents/index.d.ts +3 -2
- package/dist/coordination/agents/index.js +818 -570
- package/dist/coordination/agents/project-agent-identity.d.ts +6 -6
- package/dist/coordination/agents/role-skills.d.ts +1 -1
- package/dist/coordination/agents/types.d.ts +6 -0
- package/dist/coordination/director.d.ts +10 -2
- package/dist/coordination/fleet-manager.d.ts +48 -3
- package/dist/coordination/ifleet-manager.d.ts +2 -0
- package/dist/coordination/index.js +6880 -6440
- package/dist/coordination/mail-tools.d.ts +3 -3
- package/dist/coordination/mailbox-project-server.js +3 -4
- package/dist/coordination/mailbox-types.d.ts +6 -0
- package/dist/coordination/multi-agent-coordinator.d.ts +1 -0
- package/dist/core/agent-response.d.ts +5 -1
- package/dist/core/agent-tools.d.ts +3 -0
- package/dist/core/context.d.ts +20 -0
- package/dist/core/fallback-model.d.ts +48 -0
- package/dist/core/index.d.ts +11 -9
- package/dist/core/index.js +1119 -310
- package/dist/core/instruction-template.d.ts +83 -0
- package/dist/core/next-steps-slot.d.ts +88 -0
- package/dist/core/system-prompt-blocks.d.ts +10 -1
- package/dist/core/system-prompt-builder.d.ts +40 -4
- package/dist/core/system-prompt-memory-skills.d.ts +1 -0
- package/dist/core/system-prompt-skill-bodies.d.ts +3 -3
- package/dist/defaults/index.js +9805 -8767
- package/dist/design/index.js +11 -3
- package/dist/execution/auto-compaction-middleware.d.ts +17 -0
- package/dist/execution/autonomy-brain.d.ts +15 -2
- package/dist/execution/brain-runtime.d.ts +3 -1
- package/dist/execution/compaction-core.d.ts +41 -2
- package/dist/execution/council-brain.d.ts +47 -1
- package/dist/execution/council-orchestrator.d.ts +25 -6
- package/dist/execution/council-prompts.d.ts +12 -1
- package/dist/execution/index.d.ts +11 -10
- package/dist/execution/index.js +10507 -9189
- package/dist/execution/tool-executor.d.ts +4 -1
- package/dist/execution/topic-shift-advisor.d.ts +53 -0
- package/dist/extension/index.js +8 -2
- package/dist/extension/registry.d.ts +8 -1
- package/dist/fleet-notifier.d.ts +9 -2
- package/dist/goal/index.js +11 -3
- package/dist/hooks/index.js +50 -13
- package/dist/hooks/runner.d.ts +12 -1
- package/dist/hq/index.js +59 -21
- package/dist/hq/protocol/fleet.d.ts +20 -0
- package/dist/hq/protocol.js +10 -0
- package/dist/hq/publisher.d.ts +21 -3
- package/dist/index.d.ts +22 -20
- package/dist/index.js +12211 -8572
- package/dist/infrastructure/index.js +108 -21
- package/dist/kernel/events/brain-events.d.ts +9 -0
- package/dist/kernel/events/provider-events.d.ts +42 -1
- package/dist/kernel/events/session-events.d.ts +13 -0
- package/dist/kernel/events/tool-events.d.ts +3 -3
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +18 -1
- package/dist/models/provider-credentials.d.ts +54 -0
- package/dist/plugin/api.d.ts +6 -0
- package/dist/plugin/config.d.ts +55 -0
- package/dist/plugin/index.d.ts +2 -1
- package/dist/plugin/index.js +2179 -544
- package/dist/plugins/auto-review-plugin.d.ts +3 -0
- package/dist/plugins/cloud-config-sync-plugin.d.ts +22 -0
- package/dist/plugins/review-claim-registry.d.ts +23 -8
- package/dist/plugins/review-types.d.ts +2 -0
- package/dist/registry/index.js +109 -74
- package/dist/registry/tool-registry.d.ts +15 -0
- package/dist/security/capabilities.d.ts +2 -0
- package/dist/security/index.d.ts +2 -2
- package/dist/security/index.js +166 -44
- package/dist/security/permission-helpers.d.ts +23 -6
- package/dist/security/permission-policy.d.ts +16 -0
- package/dist/security/readonly-permission-policy.d.ts +20 -0
- package/dist/security/totp.d.ts +14 -0
- package/dist/session-registry-atomic-file.d.ts +32 -5
- package/dist/skills/frontmatter.d.ts +6 -0
- package/dist/skills/index.js +22 -5
- package/dist/storage/cloud-config-sync/sanitize.d.ts +54 -0
- package/dist/storage/cloud-config-sync.d.ts +87 -0
- package/dist/storage/director-state.d.ts +7 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +887 -74
- package/dist/tools/fallback-system-config-view-tool.d.ts +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +898 -335
- package/dist/tools/one-shot-llm-tool.d.ts +1 -0
- package/dist/tools/plugin-manager.d.ts +27 -0
- package/dist/types/config/mcp-features.d.ts +18 -11
- package/dist/types/config/root.d.ts +8 -1
- package/dist/types/config/runtime.d.ts +20 -6
- package/dist/types/config/skills-fleet-brain.d.ts +12 -4
- package/dist/types/config/tools.d.ts +16 -0
- package/dist/types/context-window.d.ts +1 -0
- package/dist/types/council.d.ts +11 -0
- package/dist/types/hooks.d.ts +14 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +1 -0
- package/dist/types/multi-agent.d.ts +12 -0
- package/dist/types/one-shot-llm.d.ts +25 -0
- package/dist/types/plugin.d.ts +28 -0
- package/dist/types/provider.d.ts +16 -0
- package/dist/types/runtime-capability-manifest.d.ts +168 -0
- package/dist/types/skill.d.ts +5 -0
- package/dist/types/system-prompt.d.ts +3 -1
- package/dist/types/tool-executor.d.ts +8 -1
- package/dist/utils/context-breakdown.d.ts +8 -2
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +141 -31
- package/dist/utils/regex-guard.d.ts +16 -2
- package/dist/utils/sage-output-block.d.ts +7 -10
- package/dist/utils/wstack-paths.d.ts +11 -3
- package/dist/worktree/index.js +4 -4
- package/instructions/agents/browser.md +1 -4
- package/instructions/agents/e2e.md +17 -15
- package/instructions/agents/ios.md +3 -3
- package/instructions/agents/search.md +1 -1
- package/instructions/autonomy/goal-preamble.md +4 -4
- package/instructions/coordination/director-preamble.md +2 -3
- package/instructions/coordination/subagent-baseline.md +3 -1
- package/instructions/leader-after-task.md +12 -0
- package/instructions/llm/chimera-review.md +1 -1
- package/instructions/modes/audit-lite.md +1 -1
- package/instructions/sections/tool/common-patterns.md +18 -2
- package/instructions/sections/tool/mailbox-compact.md +1 -1
- package/instructions/sections/tool/mailbox-full.md +1 -1
- package/instructions/system-lite.md +91 -3
- package/instructions/system-pro.md +296 -102
- package/instructions/system.md +248 -92
- package/package.json +3 -3
- package/skills/api-design/SKILL.md +3 -0
- package/skills/audit-log/SKILL.md +2 -0
- package/skills/auto-review/SKILL.md +6 -1
- package/skills/bug-hunter/SKILL.md +4 -1
- package/skills/chimera/SKILL.md +3 -0
- package/skills/data-governance/SKILL.md +3 -0
- package/skills/design-system/SKILL.md +5 -2
- package/skills/docker-deploy/SKILL.md +2 -0
- package/skills/git-flow/SKILL.md +2 -0
- package/skills/mailbox-bridge/SKILL.md +3 -0
- package/skills/mnemosyne/SKILL.md +2 -0
- package/skills/multi-agent/SKILL.md +4 -1
- package/skills/node-modern/SKILL.md +4 -1
- package/skills/observability/SKILL.md +3 -0
- package/skills/output-standards/SKILL.md +2 -0
- package/skills/plugin-author/SKILL.md +4 -1
- package/skills/prompt-engineering/SKILL.md +3 -1
- package/skills/react-modern/SKILL.md +6 -3
- package/skills/refactor-planner/SKILL.md +2 -0
- package/skills/research-web/SKILL.md +3 -0
- package/skills/sdd/SKILL.md +3 -1
- package/skills/security-scanner/SKILL.md +3 -0
- package/skills/skill-creator/SKILL.md +2 -0
- package/skills/tech-stack/SKILL.md +3 -0
- package/skills/testing/SKILL.md +4 -1
- package/skills/typescript-strict/SKILL.md +4 -1
- package/skills/wrongstack-kanban/SKILL.md +6 -3
- package/skills/wrongstack-mailbox/SKILL.md +4 -1
- package/skills/wrongstack-mailbox-mcp/SKILL.md +10 -8
package/dist/plugin/index.js
CHANGED
|
@@ -277,11 +277,11 @@ __export(review_finding_store_exports, {
|
|
|
277
277
|
JsonlFindingStore: () => JsonlFindingStore,
|
|
278
278
|
resolveFindingStorePath: () => resolveFindingStorePath
|
|
279
279
|
});
|
|
280
|
-
import { randomUUID as
|
|
281
|
-
import * as
|
|
282
|
-
import * as
|
|
280
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
281
|
+
import * as fsp3 from "node:fs/promises";
|
|
282
|
+
import * as path16 from "node:path";
|
|
283
283
|
function resolveFindingStorePath(projectDir) {
|
|
284
|
-
return
|
|
284
|
+
return path16.join(projectDir, FINDING_STORE_FILE);
|
|
285
285
|
}
|
|
286
286
|
var FINDING_RESOLVED_RETENTION_MS, FINDING_IGNORED_RETENTION_MS, FINDING_DEFAULT_PAGE_SIZE, NL, LINE_SEPARATOR, FINDING_STORE_FILE, JsonlFindingStore;
|
|
287
287
|
var init_review_finding_store = __esm({
|
|
@@ -325,7 +325,7 @@ var init_review_finding_store = __esm({
|
|
|
325
325
|
match.finding.status = "active";
|
|
326
326
|
match.finding.resolution = void 0;
|
|
327
327
|
const updatedRecord = { __finding: 1, data: match.finding };
|
|
328
|
-
await
|
|
328
|
+
await fsp3.appendFile(
|
|
329
329
|
this.filePath,
|
|
330
330
|
JSON.stringify(updatedRecord) + LINE_SEPARATOR + JSON.stringify({ __findingEvent: 1, data: reopenEvent }) + LINE_SEPARATOR,
|
|
331
331
|
{ encoding: "utf8", mode: SECRET_FILE_MODE }
|
|
@@ -339,7 +339,7 @@ var init_review_finding_store = __esm({
|
|
|
339
339
|
match.finding.status,
|
|
340
340
|
context
|
|
341
341
|
);
|
|
342
|
-
await
|
|
342
|
+
await fsp3.appendFile(
|
|
343
343
|
this.filePath,
|
|
344
344
|
JSON.stringify({ __findingEvent: 1, data: relinkEvent }) + LINE_SEPARATOR,
|
|
345
345
|
{ encoding: "utf8", mode: SECRET_FILE_MODE }
|
|
@@ -350,7 +350,7 @@ var init_review_finding_store = __esm({
|
|
|
350
350
|
const record = { __finding: 1, data: finding };
|
|
351
351
|
const createdEvent = this._makeEvent(finding.id, "created", null, "active", context);
|
|
352
352
|
const lines = JSON.stringify(record) + LINE_SEPARATOR + JSON.stringify({ __findingEvent: 1, data: createdEvent }) + LINE_SEPARATOR;
|
|
353
|
-
await
|
|
353
|
+
await fsp3.appendFile(this.filePath, lines, { encoding: "utf8", mode: SECRET_FILE_MODE });
|
|
354
354
|
result.created++;
|
|
355
355
|
}
|
|
356
356
|
}
|
|
@@ -366,7 +366,7 @@ var init_review_finding_store = __esm({
|
|
|
366
366
|
validateTransition(from, to);
|
|
367
367
|
validateResolution(to, opts?.outcome);
|
|
368
368
|
const event = {
|
|
369
|
-
id:
|
|
369
|
+
id: randomUUID4(),
|
|
370
370
|
findingId,
|
|
371
371
|
eventType: to === "resolved" ? "resolved" : to === "ignored" ? "ignored" : this._eventTypeFor(to),
|
|
372
372
|
fromStatus: from,
|
|
@@ -386,7 +386,7 @@ var init_review_finding_store = __esm({
|
|
|
386
386
|
};
|
|
387
387
|
}
|
|
388
388
|
const updatedRecord = { __finding: 1, data: entry.finding };
|
|
389
|
-
await
|
|
389
|
+
await fsp3.appendFile(
|
|
390
390
|
this.filePath,
|
|
391
391
|
JSON.stringify(updatedRecord) + LINE_SEPARATOR + JSON.stringify({ __findingEvent: 1, data: event }) + LINE_SEPARATOR,
|
|
392
392
|
{ encoding: "utf8", mode: SECRET_FILE_MODE }
|
|
@@ -503,7 +503,7 @@ var init_review_finding_store = __esm({
|
|
|
503
503
|
// ── Private helpers ──────────────────────────────────────────────
|
|
504
504
|
async _readAllLines() {
|
|
505
505
|
try {
|
|
506
|
-
const raw = await
|
|
506
|
+
const raw = await fsp3.readFile(this.filePath, "utf8");
|
|
507
507
|
return raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
|
|
508
508
|
} catch (err) {
|
|
509
509
|
if (err.code === "ENOENT") return [];
|
|
@@ -550,7 +550,7 @@ var init_review_finding_store = __esm({
|
|
|
550
550
|
}
|
|
551
551
|
_makeEvent(findingId, eventType, fromStatus, toStatus, context) {
|
|
552
552
|
return {
|
|
553
|
-
id:
|
|
553
|
+
id: randomUUID4(),
|
|
554
554
|
findingId,
|
|
555
555
|
eventType,
|
|
556
556
|
fromStatus,
|
|
@@ -634,11 +634,11 @@ __export(review_report_store_exports, {
|
|
|
634
634
|
REPORT_STORE_FILE: () => REPORT_STORE_FILE,
|
|
635
635
|
resolveReportStorePath: () => resolveReportStorePath
|
|
636
636
|
});
|
|
637
|
-
import { randomUUID as
|
|
638
|
-
import * as
|
|
639
|
-
import * as
|
|
637
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
638
|
+
import * as fsp4 from "node:fs/promises";
|
|
639
|
+
import * as path17 from "node:path";
|
|
640
640
|
function resolveReportStorePath(projectDir) {
|
|
641
|
-
return
|
|
641
|
+
return path17.join(projectDir, REPORT_STORE_FILE);
|
|
642
642
|
}
|
|
643
643
|
var REPORT_RETENTION_MS, REPORT_DEFAULT_PAGE_SIZE, NL2, REPORT_STORE_FILE, JsonlReportStore;
|
|
644
644
|
var init_review_report_store = __esm({
|
|
@@ -673,7 +673,7 @@ var init_review_report_store = __esm({
|
|
|
673
673
|
rawText: input.rawText || existing.rawText,
|
|
674
674
|
files: input.files.length > 0 ? input.files : existing.files
|
|
675
675
|
};
|
|
676
|
-
await
|
|
676
|
+
await fsp4.appendFile(this.filePath, JSON.stringify({ __report: 1, data: updated }) + NL2, {
|
|
677
677
|
encoding: "utf8",
|
|
678
678
|
mode: SECRET_FILE_MODE
|
|
679
679
|
});
|
|
@@ -697,7 +697,7 @@ var init_review_report_store = __esm({
|
|
|
697
697
|
...input.cascadeDepth !== void 0 ? { cascadeDepth: input.cascadeDepth } : {}
|
|
698
698
|
};
|
|
699
699
|
const createdEvent = {
|
|
700
|
-
id:
|
|
700
|
+
id: randomUUID6(),
|
|
701
701
|
reportId: report.id,
|
|
702
702
|
eventType: "created",
|
|
703
703
|
fromLifecycle: null,
|
|
@@ -707,7 +707,7 @@ var init_review_report_store = __esm({
|
|
|
707
707
|
timestamp: report.reviewedAt
|
|
708
708
|
};
|
|
709
709
|
const lines = JSON.stringify({ __report: 1, data: report }) + NL2 + JSON.stringify({ __reportEvent: 1, data: createdEvent }) + NL2;
|
|
710
|
-
await
|
|
710
|
+
await fsp4.appendFile(this.filePath, lines, { encoding: "utf8", mode: SECRET_FILE_MODE });
|
|
711
711
|
return report;
|
|
712
712
|
});
|
|
713
713
|
}
|
|
@@ -720,7 +720,7 @@ var init_review_report_store = __esm({
|
|
|
720
720
|
const from = this._materialize(entry).lifecycle;
|
|
721
721
|
validateReportTransition(from, to);
|
|
722
722
|
const event = {
|
|
723
|
-
id:
|
|
723
|
+
id: randomUUID6(),
|
|
724
724
|
reportId,
|
|
725
725
|
eventType: reportEventTypeFor(to),
|
|
726
726
|
fromLifecycle: from,
|
|
@@ -731,7 +731,7 @@ var init_review_report_store = __esm({
|
|
|
731
731
|
reason: opts?.reason
|
|
732
732
|
};
|
|
733
733
|
entry.report.lifecycle = to;
|
|
734
|
-
await
|
|
734
|
+
await fsp4.appendFile(this.filePath, JSON.stringify({ __reportEvent: 1, data: event }) + NL2, {
|
|
735
735
|
encoding: "utf8",
|
|
736
736
|
mode: SECRET_FILE_MODE
|
|
737
737
|
});
|
|
@@ -745,7 +745,7 @@ var init_review_report_store = __esm({
|
|
|
745
745
|
if (!entry) throw new Error(`Review report not found: ${reportId}`);
|
|
746
746
|
const materialized = this._materialize(entry);
|
|
747
747
|
const event = {
|
|
748
|
-
id:
|
|
748
|
+
id: randomUUID6(),
|
|
749
749
|
reportId,
|
|
750
750
|
eventType: "note_added",
|
|
751
751
|
fromLifecycle: materialized.lifecycle,
|
|
@@ -755,7 +755,7 @@ var init_review_report_store = __esm({
|
|
|
755
755
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
756
756
|
reason: note
|
|
757
757
|
};
|
|
758
|
-
await
|
|
758
|
+
await fsp4.appendFile(this.filePath, JSON.stringify({ __reportEvent: 1, data: event }) + NL2, {
|
|
759
759
|
encoding: "utf8",
|
|
760
760
|
mode: SECRET_FILE_MODE
|
|
761
761
|
});
|
|
@@ -850,7 +850,7 @@ var init_review_report_store = __esm({
|
|
|
850
850
|
// ── Private helpers ──────────────────────────────────────────────
|
|
851
851
|
async _readAllLines() {
|
|
852
852
|
try {
|
|
853
|
-
const raw = await
|
|
853
|
+
const raw = await fsp4.readFile(this.filePath, "utf8");
|
|
854
854
|
return raw.split(NL2).filter((l) => l.trim().length > 0);
|
|
855
855
|
} catch (err) {
|
|
856
856
|
if (err.code === "ENOENT") return [];
|
|
@@ -1078,9 +1078,15 @@ var ExtensionRegistry = class {
|
|
|
1078
1078
|
* Build a composed provider runner. Extensions with `wrapProviderRunner`
|
|
1079
1079
|
* form a middleware-style chain: the innermost extension wraps the
|
|
1080
1080
|
* default runner, each subsequent wrapper wraps the previous.
|
|
1081
|
+
*
|
|
1082
|
+
* Utility calls may exclude agent-loop-only middleware (notably
|
|
1083
|
+
* `fallback-model`) while retaining request-safety wrappers such as the
|
|
1084
|
+
* prompt firewall. One-shot orchestration owns its provider and fallback
|
|
1085
|
+
* route, so an agent-loop fallback wrapper cannot safely run against it.
|
|
1081
1086
|
*/
|
|
1082
|
-
wrapProviderRunner(inner) {
|
|
1083
|
-
const
|
|
1087
|
+
wrapProviderRunner(inner, options) {
|
|
1088
|
+
const excluded = new Set(options?.exclude);
|
|
1089
|
+
const wrappers = this.extensions.filter((e) => e.wrapProviderRunner && !excluded.has(e.name)).map((e) => ({ name: e.name, wrap: expectDefined(e.wrapProviderRunner) }));
|
|
1084
1090
|
if (wrappers.length === 0) return inner;
|
|
1085
1091
|
let composed = inner;
|
|
1086
1092
|
for (let i = wrappers.length - 1; i >= 0; i--) {
|
|
@@ -1135,10 +1141,10 @@ function validateAgainstSchema(value, schema) {
|
|
|
1135
1141
|
return { ok: errors.length === 0, errors };
|
|
1136
1142
|
}
|
|
1137
1143
|
var MAX_SCHEMA_DEPTH = 64;
|
|
1138
|
-
function walk(value, schema,
|
|
1144
|
+
function walk(value, schema, path32, errors, depth) {
|
|
1139
1145
|
if (depth > MAX_SCHEMA_DEPTH) {
|
|
1140
1146
|
errors.push({
|
|
1141
|
-
path:
|
|
1147
|
+
path: path32 || "<root>",
|
|
1142
1148
|
message: `schema nesting exceeds maximum depth (${MAX_SCHEMA_DEPTH})`
|
|
1143
1149
|
});
|
|
1144
1150
|
return;
|
|
@@ -1146,7 +1152,7 @@ function walk(value, schema, path29, errors, depth) {
|
|
|
1146
1152
|
if (schema.enum !== void 0) {
|
|
1147
1153
|
if (!enumIncludes(schema.enum, value)) {
|
|
1148
1154
|
errors.push({
|
|
1149
|
-
path:
|
|
1155
|
+
path: path32 || "<root>",
|
|
1150
1156
|
message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
|
|
1151
1157
|
});
|
|
1152
1158
|
return;
|
|
@@ -1155,7 +1161,7 @@ function walk(value, schema, path29, errors, depth) {
|
|
|
1155
1161
|
if (typeof schema.type === "string") {
|
|
1156
1162
|
if (!checkType(value, schema.type)) {
|
|
1157
1163
|
errors.push({
|
|
1158
|
-
path:
|
|
1164
|
+
path: path32 || "<root>",
|
|
1159
1165
|
message: `expected ${schema.type}, got ${describeType(value)} (${previewValue(value)})`
|
|
1160
1166
|
});
|
|
1161
1167
|
return;
|
|
@@ -1167,7 +1173,7 @@ function walk(value, schema, path29, errors, depth) {
|
|
|
1167
1173
|
if (!(req in obj)) {
|
|
1168
1174
|
const expected = schema.properties?.[req]?.type;
|
|
1169
1175
|
errors.push({
|
|
1170
|
-
path: joinPath(
|
|
1176
|
+
path: joinPath(path32, req),
|
|
1171
1177
|
message: `required property missing${typeof expected === "string" ? ` (expected ${expected})` : ""}`
|
|
1172
1178
|
});
|
|
1173
1179
|
}
|
|
@@ -1175,14 +1181,14 @@ function walk(value, schema, path29, errors, depth) {
|
|
|
1175
1181
|
if (schema.properties) {
|
|
1176
1182
|
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
1177
1183
|
if (key in obj) {
|
|
1178
|
-
walk(obj[key], subSchema, joinPath(
|
|
1184
|
+
walk(obj[key], subSchema, joinPath(path32, key), errors, depth + 1);
|
|
1179
1185
|
}
|
|
1180
1186
|
}
|
|
1181
1187
|
}
|
|
1182
1188
|
}
|
|
1183
1189
|
if (schema.type === "array" && Array.isArray(value) && schema.items) {
|
|
1184
1190
|
for (let i = 0; i < value.length; i++) {
|
|
1185
|
-
walk(value[i], schema.items, `${
|
|
1191
|
+
walk(value[i], schema.items, `${path32}[${i}]`, errors, depth + 1);
|
|
1186
1192
|
}
|
|
1187
1193
|
}
|
|
1188
1194
|
}
|
|
@@ -1275,6 +1281,36 @@ function resolvePluginConfig(input) {
|
|
|
1275
1281
|
merge(input.explicitOptions, "explicit-options");
|
|
1276
1282
|
return { options, configured, sources };
|
|
1277
1283
|
}
|
|
1284
|
+
function pluginEntryMatchesName(configuredName, name, aliases = []) {
|
|
1285
|
+
for (const candidate of [name, ...aliases]) {
|
|
1286
|
+
if (configuredName === candidate) return true;
|
|
1287
|
+
if (configuredName === `@wrongstack/plugins/${candidate}`) return true;
|
|
1288
|
+
}
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
function resolvePluginEnablement(input) {
|
|
1292
|
+
if (input.config?.features?.plugins === false) {
|
|
1293
|
+
return { enabled: false, source: "feature-flag" };
|
|
1294
|
+
}
|
|
1295
|
+
const names = [.../* @__PURE__ */ new Set([input.name, ...input.aliases ?? []])];
|
|
1296
|
+
const matches2 = input.matches ?? ((configuredName) => pluginEntryMatchesName(configuredName, input.name, input.aliases ?? []));
|
|
1297
|
+
const plugins = input.config?.plugins;
|
|
1298
|
+
if (Array.isArray(plugins)) {
|
|
1299
|
+
for (const candidate of plugins) {
|
|
1300
|
+
if (typeof candidate === "string") {
|
|
1301
|
+
if (matches2(candidate)) return { enabled: true, source: "plugin-entry" };
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (!isPluginEntry(candidate) || !matches2(candidate.name)) continue;
|
|
1305
|
+
return { enabled: candidate.enabled !== false, source: "plugin-entry" };
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
for (const name of names) {
|
|
1309
|
+
const enabled = input.config?.extensions?.[name]?.["enabled"];
|
|
1310
|
+
if (typeof enabled === "boolean") return { enabled, source: "extension" };
|
|
1311
|
+
}
|
|
1312
|
+
return { enabled: input.defaultState === "active", source: "default" };
|
|
1313
|
+
}
|
|
1278
1314
|
function resolvePluginManifestConfig(plugin, config, explicitOptions) {
|
|
1279
1315
|
return resolvePluginConfig({
|
|
1280
1316
|
name: plugin.name,
|
|
@@ -1740,11 +1776,12 @@ function wrapApiForCapabilityCheck(plugin, api, log, enforce = false) {
|
|
|
1740
1776
|
});
|
|
1741
1777
|
const wrappedLlm = caps.llm !== false || !api.llm ? api.llm : new Proxy(api.llm, {
|
|
1742
1778
|
get(target, prop, receiver) {
|
|
1743
|
-
if (prop === "complete") {
|
|
1779
|
+
if (prop === "complete" || prop === "council") {
|
|
1744
1780
|
return (prompt, options) => {
|
|
1745
|
-
violate("llm",
|
|
1746
|
-
const
|
|
1747
|
-
|
|
1781
|
+
violate("llm", `${String(prop)}(${prompt.length} chars)`);
|
|
1782
|
+
const method = target[prop];
|
|
1783
|
+
if (!method) return void 0;
|
|
1784
|
+
return options === void 0 ? method.call(target, prompt) : method.call(target, prompt, options);
|
|
1748
1785
|
};
|
|
1749
1786
|
}
|
|
1750
1787
|
return Reflect.get(target, prop, receiver);
|
|
@@ -1973,6 +2010,8 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
|
|
|
1973
2010
|
const currentModel = () => hostLLM.getModel?.() ?? hostLLM.model;
|
|
1974
2011
|
const DEFAULT_MAX_TOKENS = 2048;
|
|
1975
2012
|
const HARD_MAX_TOKENS = 32768;
|
|
2013
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
2014
|
+
const HARD_TIMEOUT_MS = 12e4;
|
|
1976
2015
|
const pluginDefaults = () => {
|
|
1977
2016
|
const extensions = currentConfig().extensions;
|
|
1978
2017
|
const raw = extensions?.[owner]?.["llm"];
|
|
@@ -1982,7 +2021,11 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
|
|
|
1982
2021
|
...typeof r["provider"] === "string" && r["provider"] ? { provider: r["provider"] } : {},
|
|
1983
2022
|
...typeof r["model"] === "string" && r["model"] ? { model: r["model"] } : {},
|
|
1984
2023
|
...typeof r["maxTokens"] === "number" ? { maxTokens: r["maxTokens"] } : {},
|
|
1985
|
-
...typeof r["temperature"] === "number" ? { temperature: r["temperature"] } : {}
|
|
2024
|
+
...typeof r["temperature"] === "number" ? { temperature: r["temperature"] } : {},
|
|
2025
|
+
...typeof r["role"] === "string" && r["role"] ? { role: r["role"] } : {},
|
|
2026
|
+
...Array.isArray(r["fallbackModels"]) && r["fallbackModels"].every((v) => typeof v === "string") ? { fallbackModels: [...r["fallbackModels"]] } : {},
|
|
2027
|
+
...typeof r["timeoutMs"] === "number" ? { timeoutMs: r["timeoutMs"] } : {},
|
|
2028
|
+
...typeof r["councilProfile"] === "string" && r["councilProfile"] ? { councilProfile: r["councilProfile"] } : {}
|
|
1986
2029
|
};
|
|
1987
2030
|
};
|
|
1988
2031
|
const resolveProvider = (name, model) => {
|
|
@@ -2019,7 +2062,7 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
|
|
|
2019
2062
|
providerCache.set(cacheKey, created);
|
|
2020
2063
|
return { provider: created, providerName };
|
|
2021
2064
|
};
|
|
2022
|
-
|
|
2065
|
+
const pluginLlm = {
|
|
2023
2066
|
defaults() {
|
|
2024
2067
|
const d = pluginDefaults();
|
|
2025
2068
|
return {
|
|
@@ -2030,12 +2073,49 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
|
|
|
2030
2073
|
async complete(prompt, opts) {
|
|
2031
2074
|
const defaults = pluginDefaults();
|
|
2032
2075
|
const model = opts?.model ?? defaults.model ?? currentModel();
|
|
2033
|
-
const
|
|
2076
|
+
const providerName = opts?.provider ?? defaults.provider ?? currentProvider().id;
|
|
2034
2077
|
const maxTokens = Math.min(
|
|
2035
2078
|
HARD_MAX_TOKENS,
|
|
2036
2079
|
opts?.maxTokens ?? defaults.maxTokens ?? DEFAULT_MAX_TOKENS
|
|
2037
2080
|
);
|
|
2038
2081
|
const temperature = opts?.temperature ?? defaults.temperature;
|
|
2082
|
+
const timeoutMs = Math.min(
|
|
2083
|
+
HARD_TIMEOUT_MS,
|
|
2084
|
+
Math.max(1, opts?.timeoutMs ?? defaults.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
2085
|
+
);
|
|
2086
|
+
if (hostLLM.oneShot) {
|
|
2087
|
+
metrics.counter("llm.calls", 1, { provider: providerName, model, engine: "one-shot" });
|
|
2088
|
+
const result = await hostLLM.oneShot({
|
|
2089
|
+
userPrompt: prompt,
|
|
2090
|
+
providerId: providerName,
|
|
2091
|
+
model,
|
|
2092
|
+
maxTokens,
|
|
2093
|
+
timeoutMs,
|
|
2094
|
+
...temperature !== void 0 ? { temperature } : {},
|
|
2095
|
+
...opts?.system ? { system: opts.system } : {},
|
|
2096
|
+
...opts?.responseFormat === "json" ? { responseFormat: { type: "json_object" } } : {},
|
|
2097
|
+
...opts?.signal ? { signal: opts.signal } : {},
|
|
2098
|
+
...opts?.role ?? defaults.role ? { role: opts?.role ?? defaults.role } : {},
|
|
2099
|
+
...opts?.fallbackModels ?? defaults.fallbackModels ? { fallbackModels: [...opts?.fallbackModels ?? defaults.fallbackModels ?? []] } : {}
|
|
2100
|
+
});
|
|
2101
|
+
if (result.error) {
|
|
2102
|
+
metrics.counter("llm.errors", 1, { provider: result.provider, model: result.model });
|
|
2103
|
+
throw new Error(result.error);
|
|
2104
|
+
}
|
|
2105
|
+
metrics.counter("llm.tokens_in", result.tokens.input);
|
|
2106
|
+
metrics.counter("llm.tokens_out", result.tokens.output);
|
|
2107
|
+
return {
|
|
2108
|
+
text: result.text,
|
|
2109
|
+
model: result.model,
|
|
2110
|
+
provider: result.provider,
|
|
2111
|
+
usage: { input: result.tokens.input, output: result.tokens.output },
|
|
2112
|
+
stopReason: result.stopReason ?? "end_turn",
|
|
2113
|
+
fromFallback: result.fromFallback,
|
|
2114
|
+
attempts: result.attempts,
|
|
2115
|
+
durationMs: result.durationMs
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
const { provider } = resolveProvider(providerName, model);
|
|
2039
2119
|
const request = {
|
|
2040
2120
|
model,
|
|
2041
2121
|
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
@@ -2044,7 +2124,8 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
|
|
|
2044
2124
|
...opts?.system ? { system: [{ type: "text", text: opts.system }] } : {},
|
|
2045
2125
|
...opts?.responseFormat === "json" ? { responseFormat: { type: "json_object" } } : {}
|
|
2046
2126
|
};
|
|
2047
|
-
const
|
|
2127
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
2128
|
+
const signal = opts?.signal ? AbortSignal.any([opts.signal, timeoutSignal]) : timeoutSignal;
|
|
2048
2129
|
metrics.counter("llm.calls", 1, { provider: providerName, model });
|
|
2049
2130
|
try {
|
|
2050
2131
|
const response = await provider.complete(request, { signal });
|
|
@@ -2069,6 +2150,33 @@ function makePluginLLM(owner, hostLLM, providerRegistry, config, getLiveConfig,
|
|
|
2069
2150
|
}
|
|
2070
2151
|
}
|
|
2071
2152
|
};
|
|
2153
|
+
const council = hostLLM.council;
|
|
2154
|
+
if (council) {
|
|
2155
|
+
pluginLlm.council = async (question, opts) => {
|
|
2156
|
+
const trimmed = question.trim();
|
|
2157
|
+
if (!trimmed) throw new Error("Plugin Council question must not be empty.");
|
|
2158
|
+
if (trimmed.length > 2e4) {
|
|
2159
|
+
throw new Error("Plugin Council question must not exceed 20000 characters.");
|
|
2160
|
+
}
|
|
2161
|
+
if ((opts?.context?.length ?? 0) > 8e4) {
|
|
2162
|
+
throw new Error("Plugin Council context must not exceed 80000 characters.");
|
|
2163
|
+
}
|
|
2164
|
+
const defaults = pluginDefaults();
|
|
2165
|
+
metrics.counter("llm.council.calls", 1, { plugin: owner });
|
|
2166
|
+
const result = await council({
|
|
2167
|
+
question: trimmed,
|
|
2168
|
+
...opts?.context ? { context: opts.context } : {},
|
|
2169
|
+
...opts?.options ? { options: opts.options } : {},
|
|
2170
|
+
...opts?.profile ?? defaults.councilProfile ? { profile: opts?.profile ?? defaults.councilProfile } : {},
|
|
2171
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
2172
|
+
});
|
|
2173
|
+
metrics.counter("llm.tokens_in", result.usage.inputTokens);
|
|
2174
|
+
metrics.counter("llm.tokens_out", result.usage.outputTokens);
|
|
2175
|
+
if (result.status === "failed") metrics.counter("llm.errors", 1, { provider: "council" });
|
|
2176
|
+
return result;
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
return pluginLlm;
|
|
2072
2180
|
}
|
|
2073
2181
|
function scopedMetrics(sink, pluginName) {
|
|
2074
2182
|
const prefix = `plugin.${pluginName}.`;
|
|
@@ -2107,8 +2215,8 @@ function definePlugin(metadata, factory) {
|
|
|
2107
2215
|
// src/plugins/auto-review-plugin.ts
|
|
2108
2216
|
import { spawn as spawn3 } from "node:child_process";
|
|
2109
2217
|
import { createHash as createHash7 } from "node:crypto";
|
|
2110
|
-
import * as
|
|
2111
|
-
import * as
|
|
2218
|
+
import * as fsp2 from "node:fs/promises";
|
|
2219
|
+
import * as path15 from "node:path";
|
|
2112
2220
|
|
|
2113
2221
|
// src/core/model-availability-calendar.ts
|
|
2114
2222
|
function logicalCalendarTarget(providerId, model) {
|
|
@@ -2470,14 +2578,327 @@ var FREEZER_EMPTY = Object.freeze([]);
|
|
|
2470
2578
|
init_error();
|
|
2471
2579
|
|
|
2472
2580
|
// src/plugins/review-claim-registry.ts
|
|
2473
|
-
import { createHash } from "node:crypto";
|
|
2581
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2582
|
+
import * as fsp from "node:fs/promises";
|
|
2583
|
+
import { hostname } from "node:os";
|
|
2584
|
+
import * as path from "node:path";
|
|
2585
|
+
|
|
2586
|
+
// src/utils/pid.ts
|
|
2587
|
+
function isPidAlive(pid) {
|
|
2588
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2589
|
+
if (pid === process.pid) return true;
|
|
2590
|
+
try {
|
|
2591
|
+
process.kill(pid, 0);
|
|
2592
|
+
return true;
|
|
2593
|
+
} catch (err) {
|
|
2594
|
+
const code = err.code;
|
|
2595
|
+
if (code === "EPERM") return true;
|
|
2596
|
+
return false;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// src/plugins/review-claim-registry.ts
|
|
2601
|
+
var CLAIMS_FILE = "review-claims.jsonl";
|
|
2602
|
+
var DEFAULT_CLAIM_TTL_MS = 30 * 60 * 1e3;
|
|
2603
|
+
var LOCK_WAIT_MS = 5e3;
|
|
2604
|
+
var LOCK_RETRY_MAX_MS = 50;
|
|
2605
|
+
var LOCK_STALE_MS = 3e4;
|
|
2606
|
+
var SAME_HOST_STALE_MS = 2 * LOCK_STALE_MS;
|
|
2607
|
+
var LOCK_RETRY_BUDGET_MS = SAME_HOST_STALE_MS + 5e3;
|
|
2608
|
+
var CLAIM_STORE_LOCK_TIMEOUT = "CLAIM_STORE_LOCK_TIMEOUT";
|
|
2609
|
+
var HOST_NAME = hostname();
|
|
2610
|
+
function lockOwnerStamp() {
|
|
2611
|
+
return `${HOST_NAME}:${process.pid}`;
|
|
2612
|
+
}
|
|
2613
|
+
var lastFallbackWarnAt = 0;
|
|
2614
|
+
var FALLBACK_WARN_INTERVAL_MS = 6e4;
|
|
2615
|
+
function warnFallbackOnce(message) {
|
|
2616
|
+
const now = Date.now();
|
|
2617
|
+
if (now - lastFallbackWarnAt < FALLBACK_WARN_INTERVAL_MS) return;
|
|
2618
|
+
lastFallbackWarnAt = now;
|
|
2619
|
+
console.warn(message);
|
|
2620
|
+
}
|
|
2621
|
+
async function breakStaleLockVerified(lockPath, verify) {
|
|
2622
|
+
if (!await verify()) return false;
|
|
2623
|
+
return await breakLockAtomically(lockPath) === true;
|
|
2624
|
+
}
|
|
2625
|
+
async function breakStaleLock(lockPath) {
|
|
2626
|
+
try {
|
|
2627
|
+
const ownerRaw = await fsp.readFile(lockPath, "utf8").catch(() => null);
|
|
2628
|
+
if (ownerRaw !== null) {
|
|
2629
|
+
const [ownerHost, pidRaw] = ownerRaw.trim().split(":", 2);
|
|
2630
|
+
const ownerPid = Number.parseInt(pidRaw ?? "", 10);
|
|
2631
|
+
if (ownerHost === HOST_NAME && Number.isInteger(ownerPid) && ownerPid > 0) {
|
|
2632
|
+
if (isPidAlive(ownerPid)) {
|
|
2633
|
+
const st2 = await fsp.stat(lockPath);
|
|
2634
|
+
if (Date.now() - st2.mtimeMs > SAME_HOST_STALE_MS) {
|
|
2635
|
+
return await breakStaleLockVerified(lockPath, async () => {
|
|
2636
|
+
const st22 = await fsp.stat(lockPath);
|
|
2637
|
+
return Date.now() - st22.mtimeMs > SAME_HOST_STALE_MS;
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
return false;
|
|
2641
|
+
}
|
|
2642
|
+
return await breakStaleLockVerified(lockPath, async () => {
|
|
2643
|
+
const content = await fsp.readFile(lockPath, "utf8").catch(() => "");
|
|
2644
|
+
return content.trim() === ownerRaw.trim();
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
const st = await fsp.stat(lockPath);
|
|
2649
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
2650
|
+
return await breakStaleLockVerified(lockPath, async () => {
|
|
2651
|
+
const st2 = await fsp.stat(lockPath);
|
|
2652
|
+
return Date.now() - st2.mtimeMs > LOCK_STALE_MS;
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
} catch {
|
|
2656
|
+
}
|
|
2657
|
+
return false;
|
|
2658
|
+
}
|
|
2659
|
+
async function breakLockAtomically(lockPath) {
|
|
2660
|
+
const tombstone = `${lockPath}.stale-${randomUUID()}.tmp`;
|
|
2661
|
+
try {
|
|
2662
|
+
await fsp.rename(lockPath, tombstone);
|
|
2663
|
+
} catch {
|
|
2664
|
+
return false;
|
|
2665
|
+
}
|
|
2666
|
+
void fsp.unlink(tombstone).catch(() => void 0);
|
|
2667
|
+
return true;
|
|
2668
|
+
}
|
|
2669
|
+
async function withStoreLock(storeDir, fn, waitMs = LOCK_WAIT_MS) {
|
|
2670
|
+
const lockPath = lockFilePath(storeDir);
|
|
2671
|
+
await fsp.mkdir(storeDir, { recursive: true });
|
|
2672
|
+
const deadline = Date.now() + waitMs;
|
|
2673
|
+
let attempt = 0;
|
|
2674
|
+
for (; ; ) {
|
|
2675
|
+
let handle = await fsp.open(lockPath, "wx").catch(() => null);
|
|
2676
|
+
if (!handle && await breakStaleLock(lockPath)) {
|
|
2677
|
+
handle = await fsp.open(lockPath, "wx").catch(() => null);
|
|
2678
|
+
}
|
|
2679
|
+
if (handle) {
|
|
2680
|
+
let stampFailed = false;
|
|
2681
|
+
try {
|
|
2682
|
+
let stamped = false;
|
|
2683
|
+
for (let stampAttempt = 0; stampAttempt < 3 && !stamped; stampAttempt++) {
|
|
2684
|
+
try {
|
|
2685
|
+
await handle.writeFile(lockOwnerStamp());
|
|
2686
|
+
stamped = true;
|
|
2687
|
+
} catch {
|
|
2688
|
+
if (stampAttempt < 2) await new Promise((resolve16) => setTimeout(resolve16, 5));
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
if (!stamped) {
|
|
2692
|
+
stampFailed = true;
|
|
2693
|
+
throw new Error("failed to stamp claim lock owner");
|
|
2694
|
+
}
|
|
2695
|
+
return await fn();
|
|
2696
|
+
} finally {
|
|
2697
|
+
await handle.close().catch(() => void 0);
|
|
2698
|
+
if (stampFailed) {
|
|
2699
|
+
await fsp.unlink(lockPath).catch(() => void 0);
|
|
2700
|
+
} else {
|
|
2701
|
+
const owner = await fsp.readFile(lockPath, "utf8").catch(() => null);
|
|
2702
|
+
if (owner !== null && owner.trim() === lockOwnerStamp()) {
|
|
2703
|
+
await fsp.unlink(lockPath).catch(() => void 0);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
const remaining = deadline - Date.now();
|
|
2709
|
+
if (remaining <= 0) {
|
|
2710
|
+
throw Object.assign(new Error("review claim store lock timeout"), {
|
|
2711
|
+
code: CLAIM_STORE_LOCK_TIMEOUT
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
const delay2 = Math.min(LOCK_RETRY_MAX_MS * (attempt + 1), remaining);
|
|
2715
|
+
await new Promise((resolve16) => setTimeout(resolve16, delay2));
|
|
2716
|
+
attempt += 1;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
var MAX_LEDGER_LINES = 1e4;
|
|
2720
|
+
var HOST_SID = randomUUID();
|
|
2474
2721
|
var claimsByEventBus = /* @__PURE__ */ new WeakMap();
|
|
2475
2722
|
var startedReviews = /* @__PURE__ */ new WeakMap();
|
|
2723
|
+
var pendingStartedReviews = /* @__PURE__ */ new WeakMap();
|
|
2476
2724
|
function fingerprint(content) {
|
|
2477
2725
|
return createHash("sha256").update(content).digest("hex");
|
|
2478
2726
|
}
|
|
2727
|
+
function normalizeKeyPart(p) {
|
|
2728
|
+
const forward = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2729
|
+
return process.platform === "win32" ? forward.toLowerCase() : forward;
|
|
2730
|
+
}
|
|
2479
2731
|
function claimKey(cwd, filePath) {
|
|
2480
|
-
return `${cwd}\0${filePath}`;
|
|
2732
|
+
return `${normalizeKeyPart(cwd)}\0${normalizeKeyPart(filePath)}`;
|
|
2733
|
+
}
|
|
2734
|
+
function resolveStore(opts, cwd) {
|
|
2735
|
+
return {
|
|
2736
|
+
storeDir: opts?.storeDir ?? path.join(cwd, ".wrongstack"),
|
|
2737
|
+
ttlMs: opts?.ttlMs ?? DEFAULT_CLAIM_TTL_MS,
|
|
2738
|
+
maxLedgerLines: opts?.maxLedgerLines ?? MAX_LEDGER_LINES
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
function claimsFilePath(storeDir) {
|
|
2742
|
+
return path.join(storeDir, CLAIMS_FILE);
|
|
2743
|
+
}
|
|
2744
|
+
function lockFilePath(storeDir) {
|
|
2745
|
+
return path.join(storeDir, `${CLAIMS_FILE}.lock`);
|
|
2746
|
+
}
|
|
2747
|
+
async function readLedger(storeDir, now) {
|
|
2748
|
+
const active = /* @__PURE__ */ new Map();
|
|
2749
|
+
const raw = await fsp.readFile(claimsFilePath(storeDir), "utf8").catch((err) => {
|
|
2750
|
+
const code = err.code;
|
|
2751
|
+
if (code === "ENOENT") return "";
|
|
2752
|
+
throw err;
|
|
2753
|
+
});
|
|
2754
|
+
for (const line of raw.split("\n")) {
|
|
2755
|
+
if (!line.trim()) continue;
|
|
2756
|
+
let entry;
|
|
2757
|
+
try {
|
|
2758
|
+
const parsed = JSON.parse(line);
|
|
2759
|
+
if (parsed && typeof parsed === "object" && (parsed.op === "claim" || parsed.op === "release") && typeof parsed.key === "string" && typeof parsed.fp === "string" && typeof parsed.sid === "string" && typeof parsed.ts === "number" && (typeof parsed.exp === "undefined" || typeof parsed.exp === "number")) {
|
|
2760
|
+
entry = parsed;
|
|
2761
|
+
}
|
|
2762
|
+
} catch {
|
|
2763
|
+
}
|
|
2764
|
+
if (!entry) continue;
|
|
2765
|
+
let bucket = active.get(entry.key);
|
|
2766
|
+
if (!bucket) {
|
|
2767
|
+
bucket = /* @__PURE__ */ new Map();
|
|
2768
|
+
active.set(entry.key, bucket);
|
|
2769
|
+
}
|
|
2770
|
+
if (entry.op === "claim") {
|
|
2771
|
+
const exp = entry.exp ?? entry.ts + DEFAULT_CLAIM_TTL_MS;
|
|
2772
|
+
if (now > exp) continue;
|
|
2773
|
+
bucket.set(entry.fp, { sid: entry.sid, ts: entry.ts, exp });
|
|
2774
|
+
} else {
|
|
2775
|
+
const existing = bucket.get(entry.fp);
|
|
2776
|
+
if (existing?.sid === entry.sid && existing.ts === entry.ts) bucket.delete(entry.fp);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
return active;
|
|
2780
|
+
}
|
|
2781
|
+
async function ledgerLineCount(storeDir) {
|
|
2782
|
+
const raw = await fsp.readFile(claimsFilePath(storeDir), "utf8").catch(() => "");
|
|
2783
|
+
return raw.split("\n").filter((l) => l.trim()).length;
|
|
2784
|
+
}
|
|
2785
|
+
async function compactLedger(storeDir, active) {
|
|
2786
|
+
const lines = [];
|
|
2787
|
+
for (const [key, fps] of active) {
|
|
2788
|
+
for (const [fp, claim] of fps) {
|
|
2789
|
+
lines.push(
|
|
2790
|
+
JSON.stringify({
|
|
2791
|
+
op: "claim",
|
|
2792
|
+
key,
|
|
2793
|
+
fp,
|
|
2794
|
+
sid: claim.sid,
|
|
2795
|
+
ts: claim.ts,
|
|
2796
|
+
exp: claim.exp
|
|
2797
|
+
})
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
const tmp = `${claimsFilePath(storeDir)}.tmp-${randomUUID()}`;
|
|
2802
|
+
await fsp.writeFile(tmp, lines.length > 0 ? `${lines.join("\n")}
|
|
2803
|
+
` : "", "utf8");
|
|
2804
|
+
let replaced = false;
|
|
2805
|
+
for (let attempt = 0; attempt < 3 && !replaced; attempt++) {
|
|
2806
|
+
try {
|
|
2807
|
+
await fsp.rename(tmp, claimsFilePath(storeDir));
|
|
2808
|
+
replaced = true;
|
|
2809
|
+
} catch {
|
|
2810
|
+
if (attempt === 2) {
|
|
2811
|
+
await fsp.copyFile(tmp, claimsFilePath(storeDir)).catch(async (copyErr) => {
|
|
2812
|
+
throw copyErr;
|
|
2813
|
+
});
|
|
2814
|
+
await fsp.unlink(tmp).catch(() => void 0);
|
|
2815
|
+
replaced = true;
|
|
2816
|
+
} else {
|
|
2817
|
+
await new Promise((resolve16) => setTimeout(resolve16, 10 * (attempt + 1)));
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
async function acquireClaims(storeDir, ttlMs, maxLedgerLines, cwd, files, waitMs = LOCK_WAIT_MS) {
|
|
2823
|
+
return withStoreLock(
|
|
2824
|
+
storeDir,
|
|
2825
|
+
async () => {
|
|
2826
|
+
const now = Date.now();
|
|
2827
|
+
const active = await readLedger(storeDir, now);
|
|
2828
|
+
const claims = [];
|
|
2829
|
+
const lines = [];
|
|
2830
|
+
for (const file of files) {
|
|
2831
|
+
const key = claimKey(cwd, file.path);
|
|
2832
|
+
const fp = fingerprint(file.content);
|
|
2833
|
+
let bucket = active.get(key);
|
|
2834
|
+
if (bucket?.has(fp)) continue;
|
|
2835
|
+
if (!bucket) {
|
|
2836
|
+
bucket = /* @__PURE__ */ new Map();
|
|
2837
|
+
active.set(key, bucket);
|
|
2838
|
+
}
|
|
2839
|
+
bucket.set(fp, { sid: HOST_SID, ts: now, exp: now + ttlMs });
|
|
2840
|
+
claims.push({ key, fingerprint: fp, sid: HOST_SID, ts: now });
|
|
2841
|
+
lines.push(
|
|
2842
|
+
JSON.stringify({
|
|
2843
|
+
op: "claim",
|
|
2844
|
+
key,
|
|
2845
|
+
fp,
|
|
2846
|
+
sid: HOST_SID,
|
|
2847
|
+
ts: now,
|
|
2848
|
+
exp: now + ttlMs
|
|
2849
|
+
})
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
if (lines.length > 0) {
|
|
2853
|
+
await fsp.appendFile(claimsFilePath(storeDir), `${lines.join("\n")}
|
|
2854
|
+
`, "utf8");
|
|
2855
|
+
try {
|
|
2856
|
+
if (await ledgerLineCount(storeDir) > maxLedgerLines) {
|
|
2857
|
+
await compactLedger(storeDir, active);
|
|
2858
|
+
}
|
|
2859
|
+
} catch (err) {
|
|
2860
|
+
warnFallbackOnce(
|
|
2861
|
+
`[review-claim-registry] ledger compaction failed (${err instanceof Error ? err.message : String(err)}); will retry on a future acquire`
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
return claims;
|
|
2866
|
+
},
|
|
2867
|
+
waitMs
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
async function releaseSharedClaims(storeDir, claims, waitMs = LOCK_WAIT_MS) {
|
|
2871
|
+
if (claims.length === 0) return;
|
|
2872
|
+
await withStoreLock(
|
|
2873
|
+
storeDir,
|
|
2874
|
+
async () => {
|
|
2875
|
+
const now = Date.now();
|
|
2876
|
+
const active = await readLedger(storeDir, now);
|
|
2877
|
+
const lines = [];
|
|
2878
|
+
for (const claim of claims) {
|
|
2879
|
+
const bucket = active.get(claim.key);
|
|
2880
|
+
const live = bucket?.get(claim.fingerprint);
|
|
2881
|
+
if (live && live.sid === claim.sid && live.ts === claim.ts) {
|
|
2882
|
+
lines.push(
|
|
2883
|
+
JSON.stringify({
|
|
2884
|
+
op: "release",
|
|
2885
|
+
key: claim.key,
|
|
2886
|
+
fp: claim.fingerprint,
|
|
2887
|
+
sid: claim.sid,
|
|
2888
|
+
// Target the ORIGINAL claim time so a late release only ever
|
|
2889
|
+
// removes the claim it owns (see readLedger release matching).
|
|
2890
|
+
ts: claim.ts
|
|
2891
|
+
})
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
if (lines.length > 0) {
|
|
2896
|
+
await fsp.appendFile(claimsFilePath(storeDir), `${lines.join("\n")}
|
|
2897
|
+
`, "utf8");
|
|
2898
|
+
}
|
|
2899
|
+
},
|
|
2900
|
+
waitMs
|
|
2901
|
+
);
|
|
2481
2902
|
}
|
|
2482
2903
|
function ledgerFor(events) {
|
|
2483
2904
|
let ledger = claimsByEventBus.get(events);
|
|
@@ -2487,68 +2908,162 @@ function ledgerFor(events) {
|
|
|
2487
2908
|
}
|
|
2488
2909
|
return ledger;
|
|
2489
2910
|
}
|
|
2490
|
-
function
|
|
2911
|
+
function claimFilesInMemory(events, cwd, files, ttlMs) {
|
|
2491
2912
|
const ledger = ledgerFor(events);
|
|
2913
|
+
const now = Date.now();
|
|
2492
2914
|
const claims = [];
|
|
2493
2915
|
for (const file of files) {
|
|
2494
2916
|
const key = claimKey(cwd, file.path);
|
|
2495
2917
|
const fileFingerprint = fingerprint(file.content);
|
|
2496
2918
|
let activeFingerprints = ledger.get(key);
|
|
2919
|
+
if (activeFingerprints) {
|
|
2920
|
+
for (const [fp, entry] of activeFingerprints) {
|
|
2921
|
+
if (now > entry.exp) activeFingerprints.delete(fp);
|
|
2922
|
+
}
|
|
2923
|
+
if (activeFingerprints.size === 0) {
|
|
2924
|
+
ledger.delete(key);
|
|
2925
|
+
activeFingerprints = void 0;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2497
2928
|
if (!activeFingerprints) {
|
|
2498
|
-
activeFingerprints = /* @__PURE__ */ new
|
|
2929
|
+
activeFingerprints = /* @__PURE__ */ new Map();
|
|
2499
2930
|
ledger.set(key, activeFingerprints);
|
|
2500
2931
|
}
|
|
2501
2932
|
if (activeFingerprints.has(fileFingerprint)) continue;
|
|
2502
|
-
activeFingerprints.
|
|
2503
|
-
claims.push({ key, fingerprint: fileFingerprint });
|
|
2933
|
+
activeFingerprints.set(fileFingerprint, { ts: now, exp: now + ttlMs });
|
|
2934
|
+
claims.push({ key, fingerprint: fileFingerprint, sid: HOST_SID, ts: now });
|
|
2504
2935
|
}
|
|
2505
2936
|
return claims;
|
|
2506
2937
|
}
|
|
2507
|
-
function
|
|
2938
|
+
function releaseClaimsInMemory(events, claims) {
|
|
2508
2939
|
const ledger = ledgerFor(events);
|
|
2509
2940
|
for (const claim of claims) {
|
|
2510
2941
|
const activeFingerprints = ledger.get(claim.key);
|
|
2511
2942
|
if (!activeFingerprints) continue;
|
|
2512
|
-
activeFingerprints.
|
|
2943
|
+
if (activeFingerprints.get(claim.fingerprint)?.ts === claim.ts) {
|
|
2944
|
+
activeFingerprints.delete(claim.fingerprint);
|
|
2945
|
+
}
|
|
2513
2946
|
if (activeFingerprints.size === 0) ledger.delete(claim.key);
|
|
2514
2947
|
}
|
|
2515
2948
|
}
|
|
2516
|
-
function
|
|
2949
|
+
async function claimFiles(events, cwd, files, opts) {
|
|
2950
|
+
const { storeDir, ttlMs, maxLedgerLines } = resolveStore(opts, cwd);
|
|
2951
|
+
try {
|
|
2952
|
+
const claims = await acquireClaims(storeDir, ttlMs, maxLedgerLines, cwd, files);
|
|
2953
|
+
return { claims, storeDir };
|
|
2954
|
+
} catch (err) {
|
|
2955
|
+
const code = err.code;
|
|
2956
|
+
if (code === CLAIM_STORE_LOCK_TIMEOUT) {
|
|
2957
|
+
try {
|
|
2958
|
+
const claims = await acquireClaims(
|
|
2959
|
+
storeDir,
|
|
2960
|
+
ttlMs,
|
|
2961
|
+
maxLedgerLines,
|
|
2962
|
+
cwd,
|
|
2963
|
+
files,
|
|
2964
|
+
LOCK_RETRY_BUDGET_MS
|
|
2965
|
+
);
|
|
2966
|
+
return { claims, storeDir };
|
|
2967
|
+
} catch (err2) {
|
|
2968
|
+
warnFallbackOnce(
|
|
2969
|
+
`[review-claim-registry] claim lock still contended after ${LOCK_WAIT_MS + LOCK_RETRY_BUDGET_MS}ms (${err2 instanceof Error ? err2.message : String(err2)}); falling back to in-memory dedup`
|
|
2970
|
+
);
|
|
2971
|
+
return { claims: claimFilesInMemory(events, cwd, files, ttlMs), storeDir: null };
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
warnFallbackOnce(
|
|
2975
|
+
`[review-claim-registry] shared claim store unavailable (${err instanceof Error ? err.message : String(err)}); falling back to in-memory dedup`
|
|
2976
|
+
);
|
|
2977
|
+
return { claims: claimFilesInMemory(events, cwd, files, ttlMs), storeDir: null };
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
async function releaseClaims(events, claims, storeDir) {
|
|
2981
|
+
if (storeDir === null) {
|
|
2982
|
+
releaseClaimsInMemory(events, claims);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
try {
|
|
2986
|
+
await releaseSharedClaims(storeDir, claims);
|
|
2987
|
+
} catch (err) {
|
|
2988
|
+
if (err.code === CLAIM_STORE_LOCK_TIMEOUT) {
|
|
2989
|
+
try {
|
|
2990
|
+
await releaseSharedClaims(storeDir, claims, LOCK_RETRY_BUDGET_MS);
|
|
2991
|
+
return;
|
|
2992
|
+
} catch {
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
warnFallbackOnce(
|
|
2996
|
+
`[review-claim-registry] shared claim release failed (${err instanceof Error ? err.message : String(err)}); claim will expire via its stored TTL`
|
|
2997
|
+
);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
async function recordStartedReview(events, payload, opts) {
|
|
2517
3001
|
if (!payload || typeof payload !== "object") return;
|
|
3002
|
+
const bundle = payload;
|
|
3003
|
+
if (startedReviews.has(bundle) || pendingStartedReviews.has(bundle)) return;
|
|
2518
3004
|
const candidate = payload;
|
|
2519
3005
|
if (typeof candidate.cwd !== "string" || !Array.isArray(candidate.files)) return;
|
|
3006
|
+
const cwd = candidate.cwd;
|
|
2520
3007
|
const files = candidate.files.filter(
|
|
2521
3008
|
(file) => Boolean(file) && typeof file === "object" && typeof file.path === "string" && typeof file.content === "string"
|
|
2522
3009
|
);
|
|
2523
|
-
const
|
|
2524
|
-
|
|
2525
|
-
|
|
3010
|
+
const pending = (async () => {
|
|
3011
|
+
try {
|
|
3012
|
+
const { claims, storeDir } = await claimFiles(events, cwd, files, opts);
|
|
3013
|
+
if (claims.length === 0) return null;
|
|
3014
|
+
const entry = { events, storeDir, claims };
|
|
3015
|
+
startedReviews.set(bundle, entry);
|
|
3016
|
+
return entry;
|
|
3017
|
+
} catch {
|
|
3018
|
+
return null;
|
|
3019
|
+
}
|
|
3020
|
+
})();
|
|
3021
|
+
pendingStartedReviews.set(bundle, pending);
|
|
3022
|
+
try {
|
|
3023
|
+
await pending;
|
|
3024
|
+
} finally {
|
|
3025
|
+
if (pendingStartedReviews.get(bundle) === pending) {
|
|
3026
|
+
pendingStartedReviews.delete(bundle);
|
|
3027
|
+
}
|
|
2526
3028
|
}
|
|
2527
3029
|
}
|
|
2528
|
-
function recordCompletedReview(events, payload) {
|
|
3030
|
+
async function recordCompletedReview(events, payload) {
|
|
2529
3031
|
if (!payload || typeof payload !== "object") return;
|
|
2530
3032
|
const bundle = payload.bundle;
|
|
2531
3033
|
if (!bundle || typeof bundle !== "object") return;
|
|
2532
|
-
|
|
3034
|
+
let startedReview = startedReviews.get(bundle);
|
|
3035
|
+
if (!startedReview) {
|
|
3036
|
+
const pending = pendingStartedReviews.get(bundle);
|
|
3037
|
+
if (pending) startedReview = await pending ?? void 0;
|
|
3038
|
+
}
|
|
2533
3039
|
if (!startedReview || startedReview.events !== events) return;
|
|
2534
|
-
releaseClaims(events, startedReview.claims);
|
|
3040
|
+
await releaseClaims(events, startedReview.claims, startedReview.storeDir);
|
|
2535
3041
|
startedReviews.delete(bundle);
|
|
2536
3042
|
}
|
|
2537
|
-
function emitReviewIfChanged(api, bundle) {
|
|
2538
|
-
const claims = claimFiles(api.events, bundle.cwd, bundle.files);
|
|
3043
|
+
async function emitReviewIfChanged(api, bundle, opts) {
|
|
3044
|
+
const { claims, storeDir } = await claimFiles(api.events, bundle.cwd, bundle.files, opts);
|
|
2539
3045
|
if (claims.length === 0) return null;
|
|
2540
|
-
const
|
|
3046
|
+
const claimed = new Set(claims.map((claim) => `${claim.key}\0${claim.fingerprint}`));
|
|
2541
3047
|
const claimedBundle = {
|
|
2542
3048
|
...bundle,
|
|
2543
|
-
|
|
3049
|
+
// Filter on key+FINGERPRINT pairs, not just key: two entries for the same
|
|
3050
|
+
// path in one batch can differ in content, and a fingerprint claimed by
|
|
3051
|
+
// another session must not ride along in the emitted bundle.
|
|
3052
|
+
files: bundle.files.filter(
|
|
3053
|
+
(file) => claimed.has(`${claimKey(bundle.cwd, file.path)}\0${fingerprint(file.content)}`)
|
|
3054
|
+
)
|
|
2544
3055
|
};
|
|
2545
|
-
startedReviews.set(claimedBundle, {
|
|
3056
|
+
startedReviews.set(claimedBundle, {
|
|
3057
|
+
events: api.events,
|
|
3058
|
+
storeDir,
|
|
3059
|
+
claims
|
|
3060
|
+
});
|
|
2546
3061
|
try {
|
|
2547
3062
|
api.emitCustom("chimera.review_needed", claimedBundle);
|
|
2548
3063
|
return claimedBundle;
|
|
2549
3064
|
} catch (err) {
|
|
2550
3065
|
startedReviews.delete(claimedBundle);
|
|
2551
|
-
releaseClaims(api.events, claims);
|
|
3066
|
+
await releaseClaims(api.events, claims, storeDir);
|
|
2552
3067
|
throw err;
|
|
2553
3068
|
}
|
|
2554
3069
|
}
|
|
@@ -2559,27 +3074,30 @@ import { spawn as spawn2 } from "node:child_process";
|
|
|
2559
3074
|
// src/chronicle/project-access.ts
|
|
2560
3075
|
import * as fsPromises from "node:fs/promises";
|
|
2561
3076
|
import * as os4 from "node:os";
|
|
2562
|
-
import * as
|
|
3077
|
+
import * as path14 from "node:path";
|
|
2563
3078
|
|
|
2564
3079
|
// src/utils/wstack-paths.ts
|
|
2565
3080
|
import { createHash as createHash2 } from "node:crypto";
|
|
2566
3081
|
import * as fs from "node:fs";
|
|
2567
3082
|
import * as os from "node:os";
|
|
2568
|
-
import * as
|
|
3083
|
+
import * as path2 from "node:path";
|
|
2569
3084
|
function canonicalProjectRoot(absRoot) {
|
|
2570
|
-
const checkoutRoot =
|
|
2571
|
-
const dotGit =
|
|
3085
|
+
const checkoutRoot = path2.resolve(absRoot);
|
|
3086
|
+
const dotGit = path2.join(checkoutRoot, ".git");
|
|
2572
3087
|
try {
|
|
2573
3088
|
if (!fs.statSync(dotGit).isFile()) return checkoutRoot;
|
|
2574
3089
|
const gitDirLine = fs.readFileSync(dotGit, "utf8").trim();
|
|
2575
3090
|
const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
|
|
2576
3091
|
if (!match?.[1]) return checkoutRoot;
|
|
2577
|
-
const gitDir =
|
|
2578
|
-
const commonDirFile =
|
|
3092
|
+
const gitDir = path2.resolve(checkoutRoot, match[1].trim());
|
|
3093
|
+
const commonDirFile = path2.join(gitDir, "commondir");
|
|
2579
3094
|
if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
|
|
2580
|
-
const commonDir =
|
|
2581
|
-
|
|
2582
|
-
|
|
3095
|
+
const commonDir = path2.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
|
|
3096
|
+
const worktreesDir = path2.dirname(gitDir);
|
|
3097
|
+
if (path2.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
|
|
3098
|
+
if (path2.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
|
|
3099
|
+
if (path2.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
|
|
3100
|
+
return path2.dirname(commonDir);
|
|
2583
3101
|
} catch {
|
|
2584
3102
|
return checkoutRoot;
|
|
2585
3103
|
}
|
|
@@ -2589,7 +3107,7 @@ function projectHash(absRoot) {
|
|
|
2589
3107
|
}
|
|
2590
3108
|
function projectSlug(absRoot) {
|
|
2591
3109
|
const identityRoot = canonicalProjectRoot(absRoot);
|
|
2592
|
-
const base = slugify(
|
|
3110
|
+
const base = slugify(path2.basename(identityRoot));
|
|
2593
3111
|
const hash = createHash2("sha256").update(identityRoot).digest("hex").slice(0, 6);
|
|
2594
3112
|
return `${base}-${hash}`;
|
|
2595
3113
|
}
|
|
@@ -2600,9 +3118,9 @@ function safeProfileName(name) {
|
|
|
2600
3118
|
const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
|
|
2601
3119
|
return safe || "default";
|
|
2602
3120
|
}
|
|
2603
|
-
function
|
|
3121
|
+
function bootstrapProfileName(globalRoot) {
|
|
2604
3122
|
try {
|
|
2605
|
-
const parsed = JSON.parse(fs.readFileSync(
|
|
3123
|
+
const parsed = JSON.parse(fs.readFileSync(path2.join(globalRoot, "config.json"), "utf8"));
|
|
2606
3124
|
return safeProfileName(
|
|
2607
3125
|
typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
|
|
2608
3126
|
);
|
|
@@ -2612,17 +3130,17 @@ function activeProfileName(globalRoot) {
|
|
|
2612
3130
|
}
|
|
2613
3131
|
function wstackGlobalRoot() {
|
|
2614
3132
|
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
2615
|
-
if (fromEnv && fromEnv.trim().length > 0) return
|
|
2616
|
-
return
|
|
3133
|
+
if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
|
|
3134
|
+
return path2.join(os.homedir(), ".wrongstack");
|
|
2617
3135
|
}
|
|
2618
3136
|
function resolveWstackPaths(opts) {
|
|
2619
|
-
const globalRoot = opts.globalRoot ?? (opts.userHome ?
|
|
3137
|
+
const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
2620
3138
|
const homeDir = opts.userHome ?? os.homedir();
|
|
2621
|
-
const profileName = safeProfileName(opts.profileName ??
|
|
2622
|
-
const profileDir =
|
|
3139
|
+
const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
|
|
3140
|
+
const profileDir = path2.join(globalRoot, "profiles", profileName);
|
|
2623
3141
|
const hash = projectHash(opts.projectRoot);
|
|
2624
3142
|
const slug = projectSlug(opts.projectRoot);
|
|
2625
|
-
const projectDir =
|
|
3143
|
+
const projectDir = path2.join(globalRoot, "projects", slug);
|
|
2626
3144
|
return {
|
|
2627
3145
|
globalRoot,
|
|
2628
3146
|
profileName,
|
|
@@ -2630,79 +3148,84 @@ function resolveWstackPaths(opts) {
|
|
|
2630
3148
|
projectRoot: opts.projectRoot,
|
|
2631
3149
|
homeDir,
|
|
2632
3150
|
configDir: profileDir,
|
|
2633
|
-
globalConfig:
|
|
2634
|
-
profilesDir:
|
|
3151
|
+
globalConfig: path2.join(globalRoot, "config.json"),
|
|
3152
|
+
profilesDir: path2.join(globalRoot, "profiles"),
|
|
2635
3153
|
profileConfig: (name) => {
|
|
2636
|
-
return
|
|
3154
|
+
return path2.join(globalRoot, "profiles", safeProfileName(name), "config.json");
|
|
2637
3155
|
},
|
|
2638
3156
|
profileStatuslineConfig: (name) => {
|
|
2639
|
-
return
|
|
3157
|
+
return path2.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
|
|
2640
3158
|
},
|
|
2641
3159
|
profileModeConfig: (name) => {
|
|
2642
|
-
return
|
|
3160
|
+
return path2.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
|
|
2643
3161
|
},
|
|
2644
3162
|
profileProviderStatus: (name) => {
|
|
2645
|
-
return
|
|
3163
|
+
return path2.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
|
|
2646
3164
|
},
|
|
2647
3165
|
profileUpdateCache: (name) => {
|
|
2648
|
-
return
|
|
3166
|
+
return path2.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
|
|
2649
3167
|
},
|
|
2650
|
-
secretsKey:
|
|
2651
|
-
globalMemory:
|
|
2652
|
-
globalSkills:
|
|
2653
|
-
globalClaudeSkills:
|
|
2654
|
-
globalDesignKits:
|
|
2655
|
-
globalPrompts:
|
|
2656
|
-
globalInstructions:
|
|
2657
|
-
promptUsage:
|
|
2658
|
-
cacheDir:
|
|
2659
|
-
modelsCache:
|
|
2660
|
-
modelsOverlayCache:
|
|
2661
|
-
historyFile:
|
|
2662
|
-
logFile:
|
|
3168
|
+
secretsKey: path2.join(globalRoot, ".key"),
|
|
3169
|
+
globalMemory: path2.join(profileDir, "memory.md"),
|
|
3170
|
+
globalSkills: path2.join(profileDir, "skills"),
|
|
3171
|
+
globalClaudeSkills: path2.join(homeDir, ".claude", "skills"),
|
|
3172
|
+
globalDesignKits: path2.join(profileDir, "design-kits"),
|
|
3173
|
+
globalPrompts: path2.join(profileDir, "prompts"),
|
|
3174
|
+
globalInstructions: path2.join(profileDir, "instructions"),
|
|
3175
|
+
promptUsage: path2.join(profileDir, "prompt-usage.json"),
|
|
3176
|
+
cacheDir: path2.join(globalRoot, "cache"),
|
|
3177
|
+
modelsCache: path2.join(globalRoot, "cache", "models.dev.json"),
|
|
3178
|
+
modelsOverlayCache: path2.join(globalRoot, "cache", "models-overlay.json"),
|
|
3179
|
+
historyFile: path2.join(profileDir, "history"),
|
|
3180
|
+
logFile: path2.join(globalRoot, "logs", "wrongstack.log"),
|
|
2663
3181
|
projectDir,
|
|
2664
|
-
projectCodebaseIndex:
|
|
2665
|
-
projectMemory:
|
|
2666
|
-
projectSessions:
|
|
2667
|
-
projectTrust:
|
|
2668
|
-
projectMeta:
|
|
2669
|
-
projectLocalConfig:
|
|
2670
|
-
inProjectConfig:
|
|
2671
|
-
inProjectAgentsFile:
|
|
2672
|
-
inProjectSkills:
|
|
2673
|
-
inProjectClaudeSkills:
|
|
2674
|
-
inProjectPrompts:
|
|
2675
|
-
inProjectInstructions:
|
|
2676
|
-
inProjectDesignKits:
|
|
2677
|
-
inProjectWorktrees:
|
|
3182
|
+
projectCodebaseIndex: path2.join(projectDir, "codebase-index"),
|
|
3183
|
+
projectMemory: path2.join(projectDir, "memory.md"),
|
|
3184
|
+
projectSessions: path2.join(projectDir, "sessions"),
|
|
3185
|
+
projectTrust: path2.join(projectDir, "trust.json"),
|
|
3186
|
+
projectMeta: path2.join(projectDir, "meta.json"),
|
|
3187
|
+
projectLocalConfig: path2.join(projectDir, "config.local.json"),
|
|
3188
|
+
inProjectConfig: path2.join(opts.projectRoot, ".wrongstack", "config.json"),
|
|
3189
|
+
inProjectAgentsFile: path2.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
|
|
3190
|
+
inProjectSkills: path2.join(opts.projectRoot, ".wrongstack", "skills"),
|
|
3191
|
+
inProjectClaudeSkills: path2.join(opts.projectRoot, ".claude", "skills"),
|
|
3192
|
+
inProjectPrompts: path2.join(opts.projectRoot, ".wrongstack", "prompts"),
|
|
3193
|
+
inProjectInstructions: path2.join(opts.projectRoot, ".wrongstack", "instructions"),
|
|
3194
|
+
inProjectDesignKits: path2.join(opts.projectRoot, ".wrongstack", "design-kits"),
|
|
3195
|
+
inProjectWorktrees: path2.join(opts.projectRoot, ".wrongstack", "worktrees"),
|
|
2678
3196
|
projectHash: hash,
|
|
2679
3197
|
projectSlug: slug,
|
|
2680
|
-
projectGoal:
|
|
2681
|
-
projectInputHistory:
|
|
2682
|
-
projectSpecs:
|
|
2683
|
-
projectTaskGraphs:
|
|
2684
|
-
projectSddSession:
|
|
2685
|
-
projectPlan:
|
|
2686
|
-
projectAutophase:
|
|
2687
|
-
projectSddBoards:
|
|
2688
|
-
projectRequirementIntakes:
|
|
2689
|
-
syncConfig:
|
|
2690
|
-
configHistoryDir:
|
|
2691
|
-
projectStatus: (
|
|
3198
|
+
projectGoal: path2.join(projectDir, "goal.json"),
|
|
3199
|
+
projectInputHistory: path2.join(projectDir, "input-history.json"),
|
|
3200
|
+
projectSpecs: path2.join(projectDir, "specs"),
|
|
3201
|
+
projectTaskGraphs: path2.join(projectDir, "task-graphs"),
|
|
3202
|
+
projectSddSession: path2.join(projectDir, "sdd-session.json"),
|
|
3203
|
+
projectPlan: path2.join(projectDir, "plan.json"),
|
|
3204
|
+
projectAutophase: path2.join(projectDir, "autophase"),
|
|
3205
|
+
projectSddBoards: path2.join(projectDir, "sdd-boards"),
|
|
3206
|
+
projectRequirementIntakes: path2.join(projectDir, "requirement-intakes"),
|
|
3207
|
+
syncConfig: path2.join(profileDir, "sync.json"),
|
|
3208
|
+
configHistoryDir: path2.join(globalRoot, "config-history"),
|
|
3209
|
+
projectStatus: (statusProjectSlug) => {
|
|
3210
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
|
|
3211
|
+
throw new Error(`Invalid project slug: ${statusProjectSlug}`);
|
|
3212
|
+
}
|
|
3213
|
+
return path2.join(globalRoot, "projects", statusProjectSlug, "status.json");
|
|
3214
|
+
}
|
|
2692
3215
|
};
|
|
2693
3216
|
}
|
|
2694
3217
|
|
|
2695
3218
|
// src/chronicle/identity.ts
|
|
2696
3219
|
import { createHash as createHash3 } from "node:crypto";
|
|
2697
3220
|
import * as os2 from "node:os";
|
|
2698
|
-
import * as
|
|
3221
|
+
import * as path3 from "node:path";
|
|
2699
3222
|
function resolveChronicleRuntimeLocation(input) {
|
|
2700
3223
|
const day = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2701
3224
|
return {
|
|
2702
|
-
installationId: stableId("installation",
|
|
3225
|
+
installationId: stableId("installation", path3.resolve(input.globalRoot)),
|
|
2703
3226
|
machineId: stableId("machine", `${os2.hostname()}\0${os2.platform()}\0${os2.arch()}`),
|
|
2704
3227
|
projectId: input.projectId,
|
|
2705
|
-
chronicleDirectory:
|
|
3228
|
+
chronicleDirectory: path3.join(input.projectDir, "chronicle"),
|
|
2706
3229
|
day
|
|
2707
3230
|
};
|
|
2708
3231
|
}
|
|
@@ -2713,10 +3236,10 @@ function stableId(prefix, value) {
|
|
|
2713
3236
|
// src/chronicle/journal.ts
|
|
2714
3237
|
init_file_permissions();
|
|
2715
3238
|
init_atomic_write();
|
|
2716
|
-
import { randomUUID } from "node:crypto";
|
|
3239
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2717
3240
|
import { createReadStream } from "node:fs";
|
|
2718
3241
|
import * as fs2 from "node:fs/promises";
|
|
2719
|
-
import * as
|
|
3242
|
+
import * as path5 from "node:path";
|
|
2720
3243
|
import { createInterface } from "node:readline";
|
|
2721
3244
|
|
|
2722
3245
|
// src/chronicle/event-hash.ts
|
|
@@ -2739,14 +3262,14 @@ function chronicleEventHash(event) {
|
|
|
2739
3262
|
}
|
|
2740
3263
|
|
|
2741
3264
|
// src/chronicle/partition-filename.ts
|
|
2742
|
-
import * as
|
|
3265
|
+
import * as path4 from "node:path";
|
|
2743
3266
|
var PARTITION_FILE_PATTERN = /^(.*\.events)(?:\.(\d{5}))?\.jsonl$/;
|
|
2744
3267
|
function partitionFamily(filePath) {
|
|
2745
|
-
const match = PARTITION_FILE_PATTERN.exec(
|
|
2746
|
-
return
|
|
3268
|
+
const match = PARTITION_FILE_PATTERN.exec(path4.basename(filePath));
|
|
3269
|
+
return path4.join(path4.dirname(filePath), match?.[1] ?? path4.basename(filePath));
|
|
2747
3270
|
}
|
|
2748
3271
|
function partitionSequence(filePath) {
|
|
2749
|
-
const match = PARTITION_FILE_PATTERN.exec(
|
|
3272
|
+
const match = PARTITION_FILE_PATTERN.exec(path4.basename(filePath));
|
|
2750
3273
|
return Number(match?.[2] ?? 0);
|
|
2751
3274
|
}
|
|
2752
3275
|
function comparePartitionPaths(left, right) {
|
|
@@ -2795,10 +3318,10 @@ var ChronicleJournal = class {
|
|
|
2795
3318
|
lastHash = GENESIS_HASH;
|
|
2796
3319
|
lastAutoPurgeAt = 0;
|
|
2797
3320
|
constructor(options) {
|
|
2798
|
-
this.basePath =
|
|
3321
|
+
this.basePath = path5.resolve(options.filePath);
|
|
2799
3322
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2800
3323
|
this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
|
|
2801
|
-
this.idFactory = options.idFactory ??
|
|
3324
|
+
this.idFactory = options.idFactory ?? randomUUID2;
|
|
2802
3325
|
this.maxPending = Math.max(1, options.maxPending ?? 1e5);
|
|
2803
3326
|
this.batchWindowMs = Math.max(0, options.batchWindowMs ?? 5);
|
|
2804
3327
|
this.maxPartitionSizeBytes = options.maxPartitionSizeBytes ?? DEFAULT_MAX_PARTITION_BYTES;
|
|
@@ -2868,14 +3391,14 @@ var ChronicleJournal = class {
|
|
|
2868
3391
|
}
|
|
2869
3392
|
await this.refreshStateFromDisk();
|
|
2870
3393
|
const cutoff = Date.now() - options.retentionDays * 864e5;
|
|
2871
|
-
const activePath =
|
|
3394
|
+
const activePath = path5.resolve(this.path);
|
|
2872
3395
|
const errors = [];
|
|
2873
3396
|
let dc = 0, db = 0, sc = 0;
|
|
2874
3397
|
const suppliedFiles = options.files === void 0 ? await collectJournalPartitions(this.basePath) : [...options.files];
|
|
2875
3398
|
const eligible = /* @__PURE__ */ new Set();
|
|
2876
3399
|
for (const suppliedPath of new Set(suppliedFiles)) {
|
|
2877
|
-
const file =
|
|
2878
|
-
if (!isJournalPartition(file,
|
|
3400
|
+
const file = path5.resolve(suppliedPath);
|
|
3401
|
+
if (!isJournalPartition(file, path5.dirname(this.basePath), this.basePath)) {
|
|
2879
3402
|
errors.push({
|
|
2880
3403
|
file: suppliedPath,
|
|
2881
3404
|
reason: "not a Chronicle journal partition in this journal directory"
|
|
@@ -3096,34 +3619,34 @@ var ChronicleJournal = class {
|
|
|
3096
3619
|
}
|
|
3097
3620
|
};
|
|
3098
3621
|
function rotatedPath(basePath, index) {
|
|
3099
|
-
const dir =
|
|
3100
|
-
const ext =
|
|
3101
|
-
const base =
|
|
3102
|
-
return
|
|
3622
|
+
const dir = path5.dirname(basePath);
|
|
3623
|
+
const ext = path5.extname(basePath);
|
|
3624
|
+
const base = path5.basename(basePath, ext);
|
|
3625
|
+
return path5.join(dir, `${base}.${String(index).padStart(5, "0")}${ext}`);
|
|
3103
3626
|
}
|
|
3104
3627
|
async function collectPartitions(basePath) {
|
|
3105
|
-
const dir =
|
|
3106
|
-
const ext =
|
|
3107
|
-
const base =
|
|
3628
|
+
const dir = path5.dirname(basePath);
|
|
3629
|
+
const ext = path5.extname(basePath);
|
|
3630
|
+
const base = path5.basename(basePath, ext);
|
|
3108
3631
|
const pattern = new RegExp(`^${escapeRegex(base)}(?:\\.\\d{5})?${escapeRegex(ext)}$`);
|
|
3109
3632
|
const result = [];
|
|
3110
3633
|
try {
|
|
3111
3634
|
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
3112
3635
|
for (const entry of entries)
|
|
3113
|
-
if (entry.isFile() && pattern.test(entry.name)) result.push(
|
|
3636
|
+
if (entry.isFile() && pattern.test(entry.name)) result.push(path5.join(dir, entry.name));
|
|
3114
3637
|
} catch {
|
|
3115
3638
|
}
|
|
3116
|
-
const baseFile =
|
|
3639
|
+
const baseFile = path5.join(dir, base + ext);
|
|
3117
3640
|
const rotated = result.filter((file) => file !== baseFile).sort((left, right) => parseIndex(left, base, ext) - parseIndex(right, base, ext));
|
|
3118
3641
|
return [baseFile, ...rotated];
|
|
3119
3642
|
}
|
|
3120
3643
|
async function collectJournalPartitions(basePath) {
|
|
3121
|
-
const directory =
|
|
3644
|
+
const directory = path5.dirname(basePath);
|
|
3122
3645
|
const result = [];
|
|
3123
3646
|
try {
|
|
3124
3647
|
const entries = await fs2.readdir(directory, { withFileTypes: true });
|
|
3125
3648
|
for (const entry of entries) {
|
|
3126
|
-
const file =
|
|
3649
|
+
const file = path5.join(directory, entry.name);
|
|
3127
3650
|
if (entry.isFile() && isJournalPartition(file, directory, basePath)) result.push(file);
|
|
3128
3651
|
}
|
|
3129
3652
|
} catch {
|
|
@@ -3131,13 +3654,13 @@ async function collectJournalPartitions(basePath) {
|
|
|
3131
3654
|
return result.sort(comparePartitionPaths);
|
|
3132
3655
|
}
|
|
3133
3656
|
function isJournalPartition(filePath, directory, basePath) {
|
|
3134
|
-
if (
|
|
3135
|
-
const fileName =
|
|
3657
|
+
if (path5.dirname(path5.resolve(filePath)) !== path5.resolve(directory)) return false;
|
|
3658
|
+
const fileName = path5.basename(filePath);
|
|
3136
3659
|
if (!basePath) return false;
|
|
3137
|
-
const baseName =
|
|
3660
|
+
const baseName = path5.basename(basePath);
|
|
3138
3661
|
const dailyFamily = /^\d{4}-\d{2}-\d{2}\.events(?:\.\d{5})?\.jsonl$/;
|
|
3139
3662
|
if (/^\d{4}-\d{2}-\d{2}\.events\.jsonl$/.test(baseName)) return dailyFamily.test(fileName);
|
|
3140
|
-
return partitionFamilyBase(
|
|
3663
|
+
return partitionFamilyBase(path5.resolve(filePath)) === partitionFamilyBase(path5.resolve(basePath));
|
|
3141
3664
|
}
|
|
3142
3665
|
function groupPartitionsByFamily(files) {
|
|
3143
3666
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -3261,12 +3784,12 @@ function isHash(value) {
|
|
|
3261
3784
|
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
3262
3785
|
}
|
|
3263
3786
|
function parseIndex(filePath, base, ext) {
|
|
3264
|
-
const suffix =
|
|
3787
|
+
const suffix = path5.basename(filePath).slice(base.length + 1, -ext.length);
|
|
3265
3788
|
return suffix ? parseInt(suffix, 10) : 0;
|
|
3266
3789
|
}
|
|
3267
3790
|
function partitionIndex(filePath, basePath) {
|
|
3268
|
-
const ext =
|
|
3269
|
-
return parseIndex(filePath,
|
|
3791
|
+
const ext = path5.extname(basePath);
|
|
3792
|
+
return parseIndex(filePath, path5.basename(basePath, ext), ext);
|
|
3270
3793
|
}
|
|
3271
3794
|
function escapeRegex(text) {
|
|
3272
3795
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -3280,8 +3803,8 @@ async function readLastEntryState(filePath) {
|
|
|
3280
3803
|
throw error;
|
|
3281
3804
|
}
|
|
3282
3805
|
try {
|
|
3283
|
-
const
|
|
3284
|
-
const size =
|
|
3806
|
+
const stat8 = await handle.stat();
|
|
3807
|
+
const size = stat8.size;
|
|
3285
3808
|
let position = size, suffix = "";
|
|
3286
3809
|
while (position > 0) {
|
|
3287
3810
|
const length = Math.min(65536, position);
|
|
@@ -3298,14 +3821,14 @@ async function readLastEntryState(filePath) {
|
|
|
3298
3821
|
return {
|
|
3299
3822
|
entry: JSON.parse(trimmed),
|
|
3300
3823
|
size,
|
|
3301
|
-
birthtimeMs:
|
|
3824
|
+
birthtimeMs: stat8.birthtimeMs
|
|
3302
3825
|
};
|
|
3303
3826
|
} catch {
|
|
3304
3827
|
}
|
|
3305
3828
|
}
|
|
3306
3829
|
suffix = lines[0] ?? "";
|
|
3307
3830
|
}
|
|
3308
|
-
return { size, birthtimeMs:
|
|
3831
|
+
return { size, birthtimeMs: stat8.birthtimeMs };
|
|
3309
3832
|
} finally {
|
|
3310
3833
|
await handle.close();
|
|
3311
3834
|
}
|
|
@@ -3323,7 +3846,7 @@ async function* streamEntriesStrict(filePath) {
|
|
|
3323
3846
|
try {
|
|
3324
3847
|
entry = JSON.parse(trimmed);
|
|
3325
3848
|
} catch {
|
|
3326
|
-
throw new Error(`invalid JSON at line ${lineNumber} in ${
|
|
3849
|
+
throw new Error(`invalid JSON at line ${lineNumber} in ${path5.basename(filePath)}`);
|
|
3327
3850
|
}
|
|
3328
3851
|
yield entry;
|
|
3329
3852
|
}
|
|
@@ -3348,7 +3871,7 @@ function errorMessage(error) {
|
|
|
3348
3871
|
init_atomic_write();
|
|
3349
3872
|
import * as fs5 from "node:fs/promises";
|
|
3350
3873
|
import { createRequire } from "node:module";
|
|
3351
|
-
import * as
|
|
3874
|
+
import * as path8 from "node:path";
|
|
3352
3875
|
|
|
3353
3876
|
// src/utils/sqlite-warning.ts
|
|
3354
3877
|
var SQLITE_EXPERIMENTAL_WARNING_RE = /sqlite is an experimental feature/i;
|
|
@@ -3377,13 +3900,13 @@ function withSqliteExperimentalWarningSuppressed(run) {
|
|
|
3377
3900
|
import { createHash as createHash5 } from "node:crypto";
|
|
3378
3901
|
import { createReadStream as createReadStream2 } from "node:fs";
|
|
3379
3902
|
import * as fs4 from "node:fs/promises";
|
|
3380
|
-
import * as
|
|
3903
|
+
import * as path7 from "node:path";
|
|
3381
3904
|
import { createInterface as createInterface2 } from "node:readline";
|
|
3382
3905
|
|
|
3383
3906
|
// src/chronicle/partition-range-cache.ts
|
|
3384
3907
|
init_atomic_write();
|
|
3385
3908
|
import * as fs3 from "node:fs/promises";
|
|
3386
|
-
import * as
|
|
3909
|
+
import * as path6 from "node:path";
|
|
3387
3910
|
var CACHE_VERSION = 1;
|
|
3388
3911
|
function isRange(value) {
|
|
3389
3912
|
if (!value || typeof value !== "object") return false;
|
|
@@ -3393,14 +3916,14 @@ function isRange(value) {
|
|
|
3393
3916
|
var ChroniclePartitionRangeCache = class {
|
|
3394
3917
|
constructor(directory) {
|
|
3395
3918
|
this.directory = directory;
|
|
3396
|
-
this.cachePath =
|
|
3919
|
+
this.cachePath = path6.join(directory, "range-cache.json");
|
|
3397
3920
|
}
|
|
3398
3921
|
directory;
|
|
3399
3922
|
cachePath;
|
|
3400
3923
|
entries;
|
|
3401
3924
|
dirty = false;
|
|
3402
3925
|
toKey(filePath) {
|
|
3403
|
-
return
|
|
3926
|
+
return path6.relative(this.directory, filePath).split(path6.sep).join("/");
|
|
3404
3927
|
}
|
|
3405
3928
|
async load() {
|
|
3406
3929
|
if (this.entries) return this.entries;
|
|
@@ -3501,7 +4024,7 @@ var ChronicleQueryEngine = class _ChronicleQueryEngine {
|
|
|
3501
4024
|
/** The one file (if any) that could still be actively appended to — never cache-checked or skipped. */
|
|
3502
4025
|
activeFile;
|
|
3503
4026
|
static async fromDirectory(directory, options) {
|
|
3504
|
-
const resolved =
|
|
4027
|
+
const resolved = path7.resolve(directory);
|
|
3505
4028
|
const files = await findPartitions(resolved);
|
|
3506
4029
|
const rangeCache = options?.rangeCache ?? new ChroniclePartitionRangeCache(resolved);
|
|
3507
4030
|
return new _ChronicleQueryEngine(
|
|
@@ -3898,7 +4421,7 @@ async function findPartitions(root) {
|
|
|
3898
4421
|
return;
|
|
3899
4422
|
}
|
|
3900
4423
|
for (const entry of entries) {
|
|
3901
|
-
const full =
|
|
4424
|
+
const full = path7.join(directory, entry.name);
|
|
3902
4425
|
if (entry.isDirectory()) await visit(full);
|
|
3903
4426
|
else if (entry.isFile() && PARTITION_FILE_PATTERN.test(entry.name)) result.push(full);
|
|
3904
4427
|
}
|
|
@@ -4085,7 +4608,7 @@ async function resolveSnapshotFiles(files, snapshot) {
|
|
|
4085
4608
|
}));
|
|
4086
4609
|
}
|
|
4087
4610
|
function fileId(file) {
|
|
4088
|
-
return createHash5("sha256").update(
|
|
4611
|
+
return createHash5("sha256").update(path7.resolve(file), "utf8").digest("base64url");
|
|
4089
4612
|
}
|
|
4090
4613
|
function stableStringify2(value) {
|
|
4091
4614
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
@@ -4197,8 +4720,8 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
|
|
|
4197
4720
|
directory;
|
|
4198
4721
|
dbPath;
|
|
4199
4722
|
constructor(directory) {
|
|
4200
|
-
this.directory =
|
|
4201
|
-
this.dbPath =
|
|
4723
|
+
this.directory = path8.resolve(directory);
|
|
4724
|
+
this.dbPath = path8.join(this.directory, "metrics.db");
|
|
4202
4725
|
const Database = loadDatabaseSync();
|
|
4203
4726
|
this.db = new Database(this.dbPath);
|
|
4204
4727
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
@@ -4223,7 +4746,7 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
|
|
|
4223
4746
|
const files = await findChroniclePartitions(this.directory);
|
|
4224
4747
|
const offsets = this.loadOffsets();
|
|
4225
4748
|
for (const file of files) {
|
|
4226
|
-
const key = normalizeKey(
|
|
4749
|
+
const key = normalizeKey(path8.relative(this.directory, file));
|
|
4227
4750
|
const consumed = offsets.get(key) ?? 0;
|
|
4228
4751
|
const ingested = await this.ingestFile(file, key, consumed, result);
|
|
4229
4752
|
if (ingested) result.sourceFiles++;
|
|
@@ -4593,7 +5116,7 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
|
|
|
4593
5116
|
}
|
|
4594
5117
|
pruneOffsets(existingFiles) {
|
|
4595
5118
|
const keep = new Set(
|
|
4596
|
-
existingFiles.map((file) => normalizeKey(
|
|
5119
|
+
existingFiles.map((file) => normalizeKey(path8.relative(this.directory, file)))
|
|
4597
5120
|
);
|
|
4598
5121
|
for (const row of this.db.prepare("SELECT file FROM ingest_state").all()) {
|
|
4599
5122
|
if (!keep.has(row.file))
|
|
@@ -4890,7 +5413,7 @@ function normalizePathKey(value) {
|
|
|
4890
5413
|
|
|
4891
5414
|
// src/chronicle/legacy-journal-import.ts
|
|
4892
5415
|
import * as fs6 from "node:fs/promises";
|
|
4893
|
-
import * as
|
|
5416
|
+
import * as path9 from "node:path";
|
|
4894
5417
|
var ChronicleImportError = class extends Error {
|
|
4895
5418
|
constructor(message, day, sequence) {
|
|
4896
5419
|
super(message);
|
|
@@ -4933,7 +5456,7 @@ async function importLegacyChronicleJournal(journal, directory) {
|
|
|
4933
5456
|
let importedFamilies = 0;
|
|
4934
5457
|
for (const familyBase of families) {
|
|
4935
5458
|
const day = dayOfFamily(familyBase);
|
|
4936
|
-
const basePath =
|
|
5459
|
+
const basePath = path9.join(directory, `${familyBase}.jsonl`);
|
|
4937
5460
|
let familyEvents = 0;
|
|
4938
5461
|
if (journal.hasImportedDay(day)) continue;
|
|
4939
5462
|
try {
|
|
@@ -4996,9 +5519,10 @@ async function importLegacyChronicleJournal(journal, directory) {
|
|
|
4996
5519
|
}
|
|
4997
5520
|
|
|
4998
5521
|
// src/chronicle/sqlite-journal.ts
|
|
4999
|
-
import { randomUUID as
|
|
5522
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
5523
|
+
import * as fs8 from "node:fs";
|
|
5000
5524
|
import { createRequire as createRequire2 } from "node:module";
|
|
5001
|
-
import * as
|
|
5525
|
+
import * as path11 from "node:path";
|
|
5002
5526
|
|
|
5003
5527
|
// src/utils/index.ts
|
|
5004
5528
|
init_atomic_write();
|
|
@@ -5027,7 +5551,7 @@ function envFlag(value) {
|
|
|
5027
5551
|
return !/^(0|false|no|off)$/i.test(value.trim());
|
|
5028
5552
|
}
|
|
5029
5553
|
var COLOR = isColorTty();
|
|
5030
|
-
var wrap = (
|
|
5554
|
+
var wrap = (open5, close) => (s) => COLOR ? `\x1B[${open5}m${s}\x1B[${close}m` : s;
|
|
5031
5555
|
var color = {
|
|
5032
5556
|
reset: wrap("0", "0"),
|
|
5033
5557
|
bold: wrap("1", "22"),
|
|
@@ -5047,6 +5571,20 @@ var color = {
|
|
|
5047
5571
|
bgGreen: wrap("42", "49")
|
|
5048
5572
|
};
|
|
5049
5573
|
|
|
5574
|
+
// src/utils/config-json.ts
|
|
5575
|
+
import * as fs7 from "node:fs/promises";
|
|
5576
|
+
async function readJsonObjectFile(filePath) {
|
|
5577
|
+
try {
|
|
5578
|
+
const parsed = JSON.parse(await fs7.readFile(filePath, "utf8"));
|
|
5579
|
+
return isJsonObject(parsed) ? parsed : {};
|
|
5580
|
+
} catch {
|
|
5581
|
+
return {};
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
function isJsonObject(value) {
|
|
5585
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5050
5588
|
// src/skills/limits.ts
|
|
5051
5589
|
var SKILL_LIMITS = {
|
|
5052
5590
|
/**
|
|
@@ -5147,7 +5685,14 @@ var SKILL_LIMITS = {
|
|
|
5147
5685
|
};
|
|
5148
5686
|
|
|
5149
5687
|
// src/skills/frontmatter.ts
|
|
5150
|
-
var SCALAR_KEYS = /* @__PURE__ */ new Set([
|
|
5688
|
+
var SCALAR_KEYS = /* @__PURE__ */ new Set([
|
|
5689
|
+
"name",
|
|
5690
|
+
"description",
|
|
5691
|
+
"trigger",
|
|
5692
|
+
"version",
|
|
5693
|
+
"license",
|
|
5694
|
+
"compatibility"
|
|
5695
|
+
]);
|
|
5151
5696
|
function parseSkillFrontmatter(raw) {
|
|
5152
5697
|
const text = normalizeLineEndings(raw);
|
|
5153
5698
|
if (!text.startsWith("---")) return {};
|
|
@@ -5206,8 +5751,9 @@ function parseFrontmatterBlock(block) {
|
|
|
5206
5751
|
out[normalizeKey2(key)] = collected.join("\n").trim();
|
|
5207
5752
|
continue;
|
|
5208
5753
|
}
|
|
5209
|
-
if (key === "allowed-tools" || key === "allowedTools") {
|
|
5210
|
-
|
|
5754
|
+
if (key === "allowed-tools" || key === "allowedTools" || key === "required-capabilities" || key === "requiredCapabilities" || key === "required-tools" || key === "requiredTools" || key === "optional-capabilities" || key === "optionalCapabilities") {
|
|
5755
|
+
const values = rest.replace(/^\[/, "").replace(/\]$/, "").split(/[\s,]+/).map(unquote).filter(Boolean);
|
|
5756
|
+
out[normalizeKey2(key)] = values;
|
|
5211
5757
|
i++;
|
|
5212
5758
|
continue;
|
|
5213
5759
|
}
|
|
@@ -5221,7 +5767,11 @@ function parseFrontmatterBlock(block) {
|
|
|
5221
5767
|
return out;
|
|
5222
5768
|
}
|
|
5223
5769
|
function normalizeKey2(key) {
|
|
5224
|
-
|
|
5770
|
+
if (key === "allowed-tools") return "allowedTools";
|
|
5771
|
+
if (key === "required-capabilities") return "requiredCapabilities";
|
|
5772
|
+
if (key === "required-tools") return "requiredTools";
|
|
5773
|
+
if (key === "optional-capabilities") return "optionalCapabilities";
|
|
5774
|
+
return key;
|
|
5225
5775
|
}
|
|
5226
5776
|
function unquote(s) {
|
|
5227
5777
|
if (s.length >= 2 && (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'"))) {
|
|
@@ -5262,7 +5812,7 @@ function capSkillBody(body) {
|
|
|
5262
5812
|
|
|
5263
5813
|
// src/utils/instruction-file.ts
|
|
5264
5814
|
import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
5265
|
-
import * as
|
|
5815
|
+
import * as path10 from "node:path";
|
|
5266
5816
|
import { fileURLToPath } from "node:url";
|
|
5267
5817
|
var textCache = /* @__PURE__ */ new Map();
|
|
5268
5818
|
var rootCandidates;
|
|
@@ -5272,7 +5822,7 @@ function readBundledInstructionText(relativePath) {
|
|
|
5272
5822
|
let resolved = "";
|
|
5273
5823
|
for (const root of instructionRootCandidates()) {
|
|
5274
5824
|
try {
|
|
5275
|
-
resolved = readFileSync2(
|
|
5825
|
+
resolved = readFileSync2(path10.join(root, relativePath), "utf8").trimEnd();
|
|
5276
5826
|
break;
|
|
5277
5827
|
} catch {
|
|
5278
5828
|
}
|
|
@@ -5288,11 +5838,11 @@ function renderInstructionTemplate(template, values) {
|
|
|
5288
5838
|
}
|
|
5289
5839
|
function instructionRootCandidates() {
|
|
5290
5840
|
if (rootCandidates !== void 0) return rootCandidates;
|
|
5291
|
-
const here =
|
|
5841
|
+
const here = path10.dirname(fileURLToPath(import.meta.url));
|
|
5292
5842
|
const candidates = [
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5843
|
+
path10.resolve(here, "../../instructions"),
|
|
5844
|
+
path10.resolve(here, "../instructions"),
|
|
5845
|
+
path10.resolve(here, "instructions")
|
|
5296
5846
|
];
|
|
5297
5847
|
rootCandidates = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
|
|
5298
5848
|
return rootCandidates;
|
|
@@ -5406,7 +5956,7 @@ var ChronicleSqliteQueryEngine = class {
|
|
|
5406
5956
|
const comparison = order === "asc" ? ">" : "<";
|
|
5407
5957
|
const keyset = cursor ? ` AND (day, sequence) ${comparison} (?, ?)` : "";
|
|
5408
5958
|
const sql = `SELECT day, sequence, payload FROM events WHERE ${pushed.clause}${keyset} ORDER BY day ${direction}, sequence ${direction} LIMIT ? OFFSET ?`;
|
|
5409
|
-
const
|
|
5959
|
+
const summary2 = createSummaryAccumulator();
|
|
5410
5960
|
const page = [];
|
|
5411
5961
|
let total = 0;
|
|
5412
5962
|
let scannedEvents = 0;
|
|
@@ -5430,7 +5980,7 @@ var ChronicleSqliteQueryEngine = class {
|
|
|
5430
5980
|
}
|
|
5431
5981
|
if (!matches(event, query)) continue;
|
|
5432
5982
|
total++;
|
|
5433
|
-
updateSummary(
|
|
5983
|
+
updateSummary(summary2, event);
|
|
5434
5984
|
if (page.length < limit) {
|
|
5435
5985
|
page.push(event);
|
|
5436
5986
|
last = { day: row.day, sequence: row.sequence };
|
|
@@ -5445,7 +5995,7 @@ var ChronicleSqliteQueryEngine = class {
|
|
|
5445
5995
|
scannedEvents,
|
|
5446
5996
|
sourceFiles: this.diagnostics.sourceFiles,
|
|
5447
5997
|
invalidLines: this.diagnostics.invalidLines,
|
|
5448
|
-
summary: finalizeSummary(
|
|
5998
|
+
summary: finalizeSummary(summary2)
|
|
5449
5999
|
};
|
|
5450
6000
|
if (total > page.length && last) {
|
|
5451
6001
|
return { ...result, nextCursor: encodeCursor2(last) };
|
|
@@ -5580,6 +6130,25 @@ var CHRONICLE_SQLITE_FILE = "chronicle.sqlite";
|
|
|
5580
6130
|
var LEGACY_JSONL_MIGRATION_KEY = "legacy-jsonl-v1";
|
|
5581
6131
|
var LEGACY_JSONL_QUARANTINE_KEY = "legacy-jsonl-v1:quarantine";
|
|
5582
6132
|
var SCHEMA_VERSION2 = 1;
|
|
6133
|
+
var SQLITE_FIXED_OVERHEAD_BYTES = 16 * 1024 * 1024;
|
|
6134
|
+
var MIN_SQLITE_PAGE_BUDGET_BYTES = 64 * 1024;
|
|
6135
|
+
var MAX_ROLLBACK_JOURNAL_RESERVE_BYTES = 64 * 1024 * 1024;
|
|
6136
|
+
var ChronicleStorageQuotaError = class extends Error {
|
|
6137
|
+
currentBytes;
|
|
6138
|
+
batchBytes;
|
|
6139
|
+
maxBytes;
|
|
6140
|
+
path;
|
|
6141
|
+
constructor(details) {
|
|
6142
|
+
super(
|
|
6143
|
+
`Chronicle SQLite quota exceeded at ${details.path}: ${details.currentBytes} live bytes + ${details.batchBytes} batch bytes exceeds ${details.maxBytes}; lower chronicle retentionDays/maxEvents to shed data (run chronicle compact to return the freed pages to the filesystem)`
|
|
6144
|
+
);
|
|
6145
|
+
this.name = "ChronicleStorageQuotaError";
|
|
6146
|
+
this.currentBytes = details.currentBytes;
|
|
6147
|
+
this.batchBytes = details.batchBytes;
|
|
6148
|
+
this.maxBytes = details.maxBytes;
|
|
6149
|
+
this.path = details.path;
|
|
6150
|
+
}
|
|
6151
|
+
};
|
|
5583
6152
|
var Ctor2;
|
|
5584
6153
|
function loadDatabaseSync2() {
|
|
5585
6154
|
if (Ctor2) return Ctor2;
|
|
@@ -5602,6 +6171,14 @@ var ChronicleSqliteJournal = class {
|
|
|
5602
6171
|
now;
|
|
5603
6172
|
monotonicNow;
|
|
5604
6173
|
idFactory;
|
|
6174
|
+
retentionDays;
|
|
6175
|
+
maxEvents;
|
|
6176
|
+
maxBytes;
|
|
6177
|
+
retentionCheckIntervalMs;
|
|
6178
|
+
nextRetentionCheckAt = 0;
|
|
6179
|
+
retainedEventCount = 0;
|
|
6180
|
+
/** Resolved lazily by `pageSizeBytes()`; constant for an open database. */
|
|
6181
|
+
cachedPageSize;
|
|
5605
6182
|
/**
|
|
5606
6183
|
* Cached chain head per day. Cleared wholesale on any write failure so the
|
|
5607
6184
|
* next append rebuilds from the database rather than trusting a counter that
|
|
@@ -5619,14 +6196,40 @@ var ChronicleSqliteJournal = class {
|
|
|
5619
6196
|
};
|
|
5620
6197
|
lastBatchDurationMs;
|
|
5621
6198
|
constructor(options) {
|
|
5622
|
-
this.dbPath =
|
|
6199
|
+
this.dbPath = path11.join(path11.resolve(options.directory), CHRONICLE_SQLITE_FILE);
|
|
5623
6200
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
5624
6201
|
this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
|
|
5625
|
-
this.idFactory = options.idFactory ?? (() =>
|
|
6202
|
+
this.idFactory = options.idFactory ?? (() => randomUUID3());
|
|
6203
|
+
if (options.retentionDays !== void 0 && (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0)) {
|
|
6204
|
+
throw new RangeError("retentionDays must be a positive finite number");
|
|
6205
|
+
}
|
|
6206
|
+
if (options.retentionCheckIntervalMs !== void 0 && (!Number.isFinite(options.retentionCheckIntervalMs) || options.retentionCheckIntervalMs <= 0)) {
|
|
6207
|
+
throw new RangeError("retentionCheckIntervalMs must be a positive finite number");
|
|
6208
|
+
}
|
|
6209
|
+
if (options.maxEvents !== void 0 && (!Number.isInteger(options.maxEvents) || options.maxEvents < 1)) {
|
|
6210
|
+
throw new RangeError("maxEvents must be a positive integer");
|
|
6211
|
+
}
|
|
6212
|
+
if (options.maxBytes !== void 0 && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)) {
|
|
6213
|
+
throw new RangeError("maxBytes must be a positive safe integer");
|
|
6214
|
+
}
|
|
6215
|
+
const minimumQuotaBytes = SQLITE_FIXED_OVERHEAD_BYTES + 2 * MIN_SQLITE_PAGE_BUDGET_BYTES;
|
|
6216
|
+
if (options.maxBytes !== void 0 && options.maxBytes < minimumQuotaBytes) {
|
|
6217
|
+
throw new RangeError(`maxBytes must be at least ${minimumQuotaBytes}`);
|
|
6218
|
+
}
|
|
6219
|
+
this.retentionDays = options.retentionDays;
|
|
6220
|
+
this.maxEvents = options.maxEvents;
|
|
6221
|
+
this.maxBytes = options.maxBytes;
|
|
6222
|
+
this.retentionCheckIntervalMs = options.retentionCheckIntervalMs ?? 60 * 60 * 1e3;
|
|
5626
6223
|
const Database = loadDatabaseSync2();
|
|
5627
6224
|
this.db = new Database(this.dbPath);
|
|
5628
|
-
this.db.exec(
|
|
6225
|
+
this.db.exec(`PRAGMA journal_mode = ${this.maxBytes === void 0 ? "WAL" : "DELETE"}`);
|
|
5629
6226
|
this.ensureSchema();
|
|
6227
|
+
this.configureByteQuota();
|
|
6228
|
+
if (this.maxEvents !== void 0) {
|
|
6229
|
+
const row = this.db.prepare("SELECT COUNT(*) AS count FROM events").get();
|
|
6230
|
+
this.retainedEventCount = row.count;
|
|
6231
|
+
this.enforceEventLimitAtStartup();
|
|
6232
|
+
}
|
|
5630
6233
|
}
|
|
5631
6234
|
close() {
|
|
5632
6235
|
this.db.close();
|
|
@@ -5685,8 +6288,10 @@ var ChronicleSqliteJournal = class {
|
|
|
5685
6288
|
events.push(event);
|
|
5686
6289
|
previous = { sequence: event.sequence, hash: event.hash };
|
|
5687
6290
|
}
|
|
6291
|
+
let retainedCountAfterCommit = this.retainedEventCount;
|
|
5688
6292
|
try {
|
|
5689
6293
|
this.db.exec("BEGIN IMMEDIATE");
|
|
6294
|
+
this.assertWithinByteQuota(events);
|
|
5690
6295
|
const insert = this.db.prepare(
|
|
5691
6296
|
`INSERT INTO events (
|
|
5692
6297
|
day, sequence, event_id, hash, previous_hash, occurred_at, event_type, outcome,
|
|
@@ -5718,6 +6323,8 @@ var ChronicleSqliteJournal = class {
|
|
|
5718
6323
|
JSON.stringify(event)
|
|
5719
6324
|
);
|
|
5720
6325
|
}
|
|
6326
|
+
retainedCountAfterCommit = this.enforceEventLimitWithinTransaction();
|
|
6327
|
+
this.assertActualAllocationWithinQuota();
|
|
5721
6328
|
this.db.exec("COMMIT");
|
|
5722
6329
|
} catch (error) {
|
|
5723
6330
|
try {
|
|
@@ -5727,11 +6334,13 @@ var ChronicleSqliteJournal = class {
|
|
|
5727
6334
|
this.anchors.clear();
|
|
5728
6335
|
this.counters.failedEvents += inputs.length;
|
|
5729
6336
|
this.lastBatchDurationMs = performance.now() - started;
|
|
5730
|
-
throw error;
|
|
6337
|
+
throw this.normalizeQuotaError(error);
|
|
5731
6338
|
}
|
|
5732
6339
|
this.anchors.set(day, previous);
|
|
5733
6340
|
this.counters.persistedEvents += events.length;
|
|
6341
|
+
this.retainedEventCount = retainedCountAfterCommit;
|
|
5734
6342
|
this.lastBatchDurationMs = performance.now() - started;
|
|
6343
|
+
await this.enforceRetentionIfDue();
|
|
5735
6344
|
return events;
|
|
5736
6345
|
}
|
|
5737
6346
|
async readAll() {
|
|
@@ -5792,31 +6401,170 @@ var ChronicleSqliteJournal = class {
|
|
|
5792
6401
|
}
|
|
5793
6402
|
return { ok: true, entries, lastSequence, lastHash };
|
|
5794
6403
|
}
|
|
6404
|
+
async enforceRetentionIfDue() {
|
|
6405
|
+
if (this.retentionDays === void 0) return;
|
|
6406
|
+
const now = this.now();
|
|
6407
|
+
if (now.getTime() < this.nextRetentionCheckAt) return;
|
|
6408
|
+
this.nextRetentionCheckAt = now.getTime() + this.retentionCheckIntervalMs;
|
|
6409
|
+
try {
|
|
6410
|
+
await this.purge({ retentionDays: this.retentionDays });
|
|
6411
|
+
} catch {
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6414
|
+
configureByteQuota() {
|
|
6415
|
+
if (this.maxBytes === void 0) {
|
|
6416
|
+
this.db.exec("PRAGMA max_page_count = 2147483646");
|
|
6417
|
+
return;
|
|
6418
|
+
}
|
|
6419
|
+
const halfSplit = Math.floor((this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES) / 2);
|
|
6420
|
+
const journalReserve = Math.min(MAX_ROLLBACK_JOURNAL_RESERVE_BYTES, halfSplit);
|
|
6421
|
+
const mainBudget = this.maxBytes - SQLITE_FIXED_OVERHEAD_BYTES - journalReserve;
|
|
6422
|
+
if (mainBudget < MIN_SQLITE_PAGE_BUDGET_BYTES) {
|
|
6423
|
+
throw new Error(
|
|
6424
|
+
`maxBytes must be at least ${SQLITE_FIXED_OVERHEAD_BYTES + 2 * MIN_SQLITE_PAGE_BUDGET_BYTES}`
|
|
6425
|
+
);
|
|
6426
|
+
}
|
|
6427
|
+
const maxPages = Math.max(1, Math.floor(mainBudget / this.pageSizeBytes()));
|
|
6428
|
+
this.db.exec(`PRAGMA max_page_count = ${maxPages}`);
|
|
6429
|
+
}
|
|
5795
6430
|
/**
|
|
5796
|
-
*
|
|
6431
|
+
* Bytes the journal actually occupies: live pages in the main database plus
|
|
6432
|
+
* whatever the rollback/WAL sidecars currently hold.
|
|
5797
6433
|
*
|
|
5798
|
-
*
|
|
5799
|
-
*
|
|
5800
|
-
*
|
|
5801
|
-
*
|
|
5802
|
-
*
|
|
6434
|
+
* The quota MUST be measured this way rather than by `statSync` on the main
|
|
6435
|
+
* database file. SQLite never returns freed pages to the filesystem — it
|
|
6436
|
+
* parks them on the freelist and reuses them — so a database that once grew
|
|
6437
|
+
* large keeps that size forever, even after retention and `maxEvents` have
|
|
6438
|
+
* evicted almost everything. Measuring the file instead wedged the journal
|
|
6439
|
+
* permanently: a 3.9 GB file whose live data was 243 MB failed a 512 MB
|
|
6440
|
+
* quota on EVERY append, and the only escape (`chronicle compact`) refuses
|
|
6441
|
+
* to run while the daemon is up — and the daemon respawns on demand. Live
|
|
6442
|
+
* pages shrink when retention deletes rows, so the quota can recover on its
|
|
6443
|
+
* own; allocated size cannot.
|
|
6444
|
+
*
|
|
6445
|
+
* The sidecars keep their on-disk size: they are transient and genuinely
|
|
6446
|
+
* bounded by `configureByteQuota`'s half-of-budget split.
|
|
5803
6447
|
*/
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
const cutoff = new Date(this.now().getTime() - options.retentionDays * 864e5).toISOString().slice(0, 10);
|
|
5813
|
-
const count = this.db.prepare("SELECT COUNT(*) AS n FROM events WHERE day < ?").get(cutoff).n;
|
|
5814
|
-
if (count === 0) return empty;
|
|
5815
|
-
if (options.dryRun) {
|
|
5816
|
-
const days = this.db.prepare("SELECT DISTINCT day FROM events WHERE day < ? ORDER BY day").all(cutoff);
|
|
5817
|
-
return { ...empty, deletedCount: count, candidates: days.map((row) => row.day) };
|
|
6448
|
+
aggregateLiveBytes() {
|
|
6449
|
+
let total = 0;
|
|
6450
|
+
for (const file of [`${this.dbPath}-journal`, `${this.dbPath}-wal`, `${this.dbPath}-shm`]) {
|
|
6451
|
+
try {
|
|
6452
|
+
total += fs8.statSync(file).size;
|
|
6453
|
+
} catch (error) {
|
|
6454
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
6455
|
+
}
|
|
5818
6456
|
}
|
|
5819
|
-
|
|
6457
|
+
return total + this.mainDatabaseLiveBytes();
|
|
6458
|
+
}
|
|
6459
|
+
/** Live (non-freelist) pages of the main database, in bytes. */
|
|
6460
|
+
mainDatabaseLiveBytes() {
|
|
6461
|
+
const pageCount = Number(
|
|
6462
|
+
this.db.prepare("PRAGMA page_count").get().page_count
|
|
6463
|
+
);
|
|
6464
|
+
const freelist = Number(
|
|
6465
|
+
this.db.prepare("PRAGMA freelist_count").get().freelist_count
|
|
6466
|
+
);
|
|
6467
|
+
return Math.max(0, pageCount - freelist) * this.pageSizeBytes();
|
|
6468
|
+
}
|
|
6469
|
+
/** Page size never changes for an open database; resolve it once. */
|
|
6470
|
+
pageSizeBytes() {
|
|
6471
|
+
if (this.cachedPageSize === void 0) {
|
|
6472
|
+
this.cachedPageSize = Number(
|
|
6473
|
+
this.db.prepare("PRAGMA page_size").get().page_size
|
|
6474
|
+
);
|
|
6475
|
+
}
|
|
6476
|
+
return this.cachedPageSize;
|
|
6477
|
+
}
|
|
6478
|
+
normalizeQuotaError(error) {
|
|
6479
|
+
if (this.maxBytes === void 0 || !(error instanceof Error)) return error;
|
|
6480
|
+
const sqliteError = error;
|
|
6481
|
+
const quotaExhausted = sqliteError.code === "SQLITE_FULL" || sqliteError.code === 13 || sqliteError.errcode === 13 || /database or disk is full/i.test(sqliteError.message);
|
|
6482
|
+
if (!quotaExhausted) return error;
|
|
6483
|
+
return new ChronicleStorageQuotaError({
|
|
6484
|
+
currentBytes: this.aggregateLiveBytes(),
|
|
6485
|
+
batchBytes: 0,
|
|
6486
|
+
maxBytes: this.maxBytes,
|
|
6487
|
+
path: this.dbPath
|
|
6488
|
+
});
|
|
6489
|
+
}
|
|
6490
|
+
assertActualAllocationWithinQuota() {
|
|
6491
|
+
if (this.maxBytes === void 0) return;
|
|
6492
|
+
const currentBytes = this.aggregateLiveBytes();
|
|
6493
|
+
if (currentBytes <= this.maxBytes) return;
|
|
6494
|
+
throw new ChronicleStorageQuotaError({
|
|
6495
|
+
currentBytes,
|
|
6496
|
+
batchBytes: 0,
|
|
6497
|
+
maxBytes: this.maxBytes,
|
|
6498
|
+
path: this.dbPath
|
|
6499
|
+
});
|
|
6500
|
+
}
|
|
6501
|
+
assertWithinByteQuota(events) {
|
|
6502
|
+
if (this.maxBytes === void 0) return;
|
|
6503
|
+
const currentBytes = this.aggregateLiveBytes();
|
|
6504
|
+
const batchBytes = Buffer.byteLength(JSON.stringify(events), "utf8");
|
|
6505
|
+
if (currentBytes + batchBytes <= this.maxBytes) return;
|
|
6506
|
+
throw new ChronicleStorageQuotaError({
|
|
6507
|
+
currentBytes,
|
|
6508
|
+
batchBytes,
|
|
6509
|
+
maxBytes: this.maxBytes,
|
|
6510
|
+
path: this.dbPath
|
|
6511
|
+
});
|
|
6512
|
+
}
|
|
6513
|
+
enforceEventLimitWithinTransaction() {
|
|
6514
|
+
const row = this.db.prepare("SELECT COUNT(*) AS count FROM events").get();
|
|
6515
|
+
if (this.maxEvents === void 0 || row.count <= this.maxEvents) return row.count;
|
|
6516
|
+
const excess = row.count - this.maxEvents;
|
|
6517
|
+
const boundary = this.db.prepare("SELECT day, sequence, hash FROM events ORDER BY day, sequence LIMIT 1 OFFSET ?").get(excess - 1);
|
|
6518
|
+
if (!boundary) return row.count;
|
|
6519
|
+
this.db.prepare(
|
|
6520
|
+
`INSERT INTO chain_checkpoint (day, sequence, hash) VALUES (?, ?, ?)
|
|
6521
|
+
ON CONFLICT(day) DO UPDATE SET sequence = excluded.sequence, hash = excluded.hash`
|
|
6522
|
+
).run(boundary.day, boundary.sequence, boundary.hash);
|
|
6523
|
+
this.db.prepare("DELETE FROM events WHERE day < ? OR (day = ? AND sequence <= ?)").run(boundary.day, boundary.day, boundary.sequence);
|
|
6524
|
+
this.db.prepare("DELETE FROM chain_checkpoint WHERE day < ?").run(boundary.day);
|
|
6525
|
+
this.anchors.clear();
|
|
6526
|
+
return this.maxEvents;
|
|
6527
|
+
}
|
|
6528
|
+
enforceEventLimitAtStartup() {
|
|
6529
|
+
if (this.maxEvents === void 0 || this.retainedEventCount <= this.maxEvents) return;
|
|
6530
|
+
try {
|
|
6531
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
6532
|
+
const retainedCount = this.enforceEventLimitWithinTransaction();
|
|
6533
|
+
this.db.exec("COMMIT");
|
|
6534
|
+
this.retainedEventCount = retainedCount;
|
|
6535
|
+
} catch (error) {
|
|
6536
|
+
try {
|
|
6537
|
+
this.db.exec("ROLLBACK");
|
|
6538
|
+
} catch {
|
|
6539
|
+
}
|
|
6540
|
+
throw error;
|
|
6541
|
+
}
|
|
6542
|
+
}
|
|
6543
|
+
/**
|
|
6544
|
+
* Drop events older than the retention window.
|
|
6545
|
+
*
|
|
6546
|
+
* Retention is day-granular and chains are day-scoped, so a purge removes
|
|
6547
|
+
* whole chains rather than truncating one. That is why nothing needs to be
|
|
6548
|
+
* checkpointed here: there is no surviving suffix left dangling without an
|
|
6549
|
+
* anchor. Any checkpoint imported from a partially-purged legacy day family
|
|
6550
|
+
* is dropped alongside its events.
|
|
6551
|
+
*/
|
|
6552
|
+
async purge(options) {
|
|
6553
|
+
const empty = {
|
|
6554
|
+
deletedCount: 0,
|
|
6555
|
+
deletedBytes: 0,
|
|
6556
|
+
skippedCount: 0,
|
|
6557
|
+
errors: []
|
|
6558
|
+
};
|
|
6559
|
+
if (!Number.isFinite(options.retentionDays) || options.retentionDays <= 0) return empty;
|
|
6560
|
+
const cutoff = new Date(this.now().getTime() - options.retentionDays * 864e5).toISOString().slice(0, 10);
|
|
6561
|
+
const count = this.db.prepare("SELECT COUNT(*) AS n FROM events WHERE day < ?").get(cutoff).n;
|
|
6562
|
+
if (count === 0) return empty;
|
|
6563
|
+
if (options.dryRun) {
|
|
6564
|
+
const days = this.db.prepare("SELECT DISTINCT day FROM events WHERE day < ? ORDER BY day").all(cutoff);
|
|
6565
|
+
return { ...empty, deletedCount: count, candidates: days.map((row) => row.day) };
|
|
6566
|
+
}
|
|
6567
|
+
try {
|
|
5820
6568
|
this.db.exec("BEGIN IMMEDIATE");
|
|
5821
6569
|
this.db.prepare("DELETE FROM events WHERE day < ?").run(cutoff);
|
|
5822
6570
|
this.db.prepare("DELETE FROM chain_checkpoint WHERE day < ?").run(cutoff);
|
|
@@ -5837,6 +6585,7 @@ var ChronicleSqliteJournal = class {
|
|
|
5837
6585
|
};
|
|
5838
6586
|
}
|
|
5839
6587
|
this.anchors.clear();
|
|
6588
|
+
this.retainedEventCount = Math.max(0, this.retainedEventCount - count);
|
|
5840
6589
|
return { ...empty, deletedCount: count };
|
|
5841
6590
|
}
|
|
5842
6591
|
/**
|
|
@@ -5868,7 +6617,9 @@ var ChronicleSqliteJournal = class {
|
|
|
5868
6617
|
ON CONFLICT(day) DO UPDATE SET sequence = excluded.sequence, hash = excluded.hash`
|
|
5869
6618
|
);
|
|
5870
6619
|
this.db.exec("BEGIN IMMEDIATE");
|
|
6620
|
+
let retainedCountAfterCommit = this.retainedEventCount;
|
|
5871
6621
|
try {
|
|
6622
|
+
this.assertWithinByteQuota([]);
|
|
5872
6623
|
await load({
|
|
5873
6624
|
insert: (day, event) => {
|
|
5874
6625
|
const row = projectEvent(event);
|
|
@@ -5898,16 +6649,19 @@ var ChronicleSqliteJournal = class {
|
|
|
5898
6649
|
checkpoint.run(day, sequence, hash);
|
|
5899
6650
|
}
|
|
5900
6651
|
});
|
|
6652
|
+
retainedCountAfterCommit = this.enforceEventLimitWithinTransaction();
|
|
6653
|
+
this.assertActualAllocationWithinQuota();
|
|
5901
6654
|
this.db.exec("COMMIT");
|
|
5902
6655
|
} catch (error) {
|
|
5903
6656
|
try {
|
|
5904
6657
|
this.db.exec("ROLLBACK");
|
|
5905
6658
|
} catch {
|
|
5906
6659
|
}
|
|
5907
|
-
throw error;
|
|
6660
|
+
throw this.normalizeQuotaError(error);
|
|
5908
6661
|
} finally {
|
|
5909
6662
|
this.anchors.clear();
|
|
5910
6663
|
}
|
|
6664
|
+
this.retainedEventCount = retainedCountAfterCommit;
|
|
5911
6665
|
}
|
|
5912
6666
|
/**
|
|
5913
6667
|
* A read engine over this journal's own connection.
|
|
@@ -6063,15 +6817,15 @@ function projectEvent(event) {
|
|
|
6063
6817
|
|
|
6064
6818
|
// src/chronicle/project-server-client.ts
|
|
6065
6819
|
import { spawn } from "node:child_process";
|
|
6066
|
-
import * as
|
|
6820
|
+
import * as fs9 from "node:fs";
|
|
6067
6821
|
import * as net from "node:net";
|
|
6068
|
-
import * as
|
|
6822
|
+
import * as path13 from "node:path";
|
|
6069
6823
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6070
6824
|
|
|
6071
6825
|
// src/chronicle/project-server-endpoint.ts
|
|
6072
6826
|
import { createHash as createHash6 } from "node:crypto";
|
|
6073
6827
|
import * as os3 from "node:os";
|
|
6074
|
-
import * as
|
|
6828
|
+
import * as path12 from "node:path";
|
|
6075
6829
|
|
|
6076
6830
|
// src/chronicle/project-server-protocol.ts
|
|
6077
6831
|
var CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION = 2;
|
|
@@ -6084,39 +6838,39 @@ function encodeChronicleProjectServerMessage(message) {
|
|
|
6084
6838
|
// src/chronicle/project-server-endpoint.ts
|
|
6085
6839
|
var CHRONICLE_PROJECT_SERVER_METADATA_FILE = "server.json";
|
|
6086
6840
|
function normalizedPath(value) {
|
|
6087
|
-
const resolved =
|
|
6841
|
+
const resolved = path12.resolve(value);
|
|
6088
6842
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
6089
6843
|
}
|
|
6090
6844
|
function chronicleProjectServerKey(projectDir) {
|
|
6091
|
-
return createHash6("sha256").update(normalizedPath(
|
|
6845
|
+
return createHash6("sha256").update(normalizedPath(path12.join(projectDir, "chronicle"))).digest("hex").slice(0, 24);
|
|
6092
6846
|
}
|
|
6093
6847
|
function chronicleProjectServerEndpoint(projectDir) {
|
|
6094
6848
|
const key = chronicleProjectServerKey(projectDir);
|
|
6095
6849
|
if (process.platform === "win32") {
|
|
6096
6850
|
return `\\\\.\\pipe\\wrongstack-chronicle-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
|
|
6097
6851
|
}
|
|
6098
|
-
return
|
|
6852
|
+
return path12.join(
|
|
6099
6853
|
os3.tmpdir(),
|
|
6100
6854
|
`wsch-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}`,
|
|
6101
6855
|
`${key}.sock`
|
|
6102
6856
|
);
|
|
6103
6857
|
}
|
|
6104
6858
|
function chronicleProjectServerMetadataPath(projectDir) {
|
|
6105
|
-
return
|
|
6859
|
+
return path12.join(projectDir, "chronicle", CHRONICLE_PROJECT_SERVER_METADATA_FILE);
|
|
6106
6860
|
}
|
|
6107
6861
|
|
|
6108
6862
|
// src/chronicle/project-server-client.ts
|
|
6109
6863
|
var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
|
|
6110
6864
|
var SERVER_START_TIMEOUT_MS = 1e4;
|
|
6111
6865
|
var DEFAULT_CALL_TIMEOUT_MS = 3e4;
|
|
6112
|
-
function resolveChronicleDaemonAvailability(moduleUrl = import.meta.url, exists =
|
|
6866
|
+
function resolveChronicleDaemonAvailability(moduleUrl = import.meta.url, exists = fs9.existsSync) {
|
|
6113
6867
|
if (process.env["WRONGSTACK_CHRONICLE_INLINE"] || process.env["WRONGSTACK_CHRONICLE_SERVER"] === "0") {
|
|
6114
6868
|
return { kind: "inline-requested" };
|
|
6115
6869
|
}
|
|
6116
6870
|
const url = locateChronicleProjectServer(moduleUrl, exists);
|
|
6117
6871
|
return url ? { kind: "available", url } : { kind: "missing-build" };
|
|
6118
6872
|
}
|
|
6119
|
-
function resolveChronicleProjectServerUrl(moduleUrl = import.meta.url, exists =
|
|
6873
|
+
function resolveChronicleProjectServerUrl(moduleUrl = import.meta.url, exists = fs9.existsSync) {
|
|
6120
6874
|
const availability = resolveChronicleDaemonAvailability(moduleUrl, exists);
|
|
6121
6875
|
return availability.kind === "available" ? availability.url : null;
|
|
6122
6876
|
}
|
|
@@ -6285,7 +7039,7 @@ var ChronicleProjectServerClient = class {
|
|
|
6285
7039
|
currentAuthToken() {
|
|
6286
7040
|
if (this.authToken === void 0) {
|
|
6287
7041
|
try {
|
|
6288
|
-
const raw =
|
|
7042
|
+
const raw = fs9.readFileSync(
|
|
6289
7043
|
chronicleProjectServerMetadataPath(this.options.projectDir),
|
|
6290
7044
|
"utf8"
|
|
6291
7045
|
);
|
|
@@ -6423,7 +7177,7 @@ var ChronicleProjectServerClient = class {
|
|
|
6423
7177
|
}
|
|
6424
7178
|
};
|
|
6425
7179
|
function normalizePath(value) {
|
|
6426
|
-
const resolved =
|
|
7180
|
+
const resolved = path13.resolve(value);
|
|
6427
7181
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
6428
7182
|
}
|
|
6429
7183
|
|
|
@@ -6431,7 +7185,7 @@ function normalizePath(value) {
|
|
|
6431
7185
|
function resolveChronicleProjectServerOptions(options) {
|
|
6432
7186
|
if (options.projectPaths) {
|
|
6433
7187
|
return {
|
|
6434
|
-
projectRoot:
|
|
7188
|
+
projectRoot: path14.resolve(options.projectRoot),
|
|
6435
7189
|
...options.projectPaths,
|
|
6436
7190
|
...options.retentionDays !== void 0 ? { retentionDays: options.retentionDays } : {}
|
|
6437
7191
|
};
|
|
@@ -6441,7 +7195,7 @@ function resolveChronicleProjectServerOptions(options) {
|
|
|
6441
7195
|
userHome: options.userHome ?? os4.homedir()
|
|
6442
7196
|
});
|
|
6443
7197
|
return {
|
|
6444
|
-
projectRoot:
|
|
7198
|
+
projectRoot: path14.resolve(options.projectRoot),
|
|
6445
7199
|
globalRoot: paths.globalRoot,
|
|
6446
7200
|
projectId: paths.projectHash,
|
|
6447
7201
|
projectDir: paths.projectDir,
|
|
@@ -6469,7 +7223,7 @@ function assertInlineWasRequested(caller) {
|
|
|
6469
7223
|
);
|
|
6470
7224
|
}
|
|
6471
7225
|
function legacyPartitionPath(location) {
|
|
6472
|
-
return
|
|
7226
|
+
return path14.join(location.chronicleDirectory, `${location.day}.events.jsonl`);
|
|
6473
7227
|
}
|
|
6474
7228
|
function useSqliteStore() {
|
|
6475
7229
|
return process.env["WRONGSTACK_CHRONICLE_STORE"] !== "jsonl";
|
|
@@ -6575,7 +7329,7 @@ var InlineChronicleProjectAccess = class {
|
|
|
6575
7329
|
}
|
|
6576
7330
|
}
|
|
6577
7331
|
get chronicleDirectory() {
|
|
6578
|
-
return
|
|
7332
|
+
return path14.join(this.options.projectDir, "chronicle");
|
|
6579
7333
|
}
|
|
6580
7334
|
journalForToday() {
|
|
6581
7335
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -6784,16 +7538,16 @@ async function findActiveKanbanCard(projectRoot) {
|
|
|
6784
7538
|
}
|
|
6785
7539
|
async function findFileProvenance(projectRoot, filePaths) {
|
|
6786
7540
|
if (filePaths.length === 0) return [];
|
|
6787
|
-
let
|
|
7541
|
+
let access7;
|
|
6788
7542
|
try {
|
|
6789
|
-
|
|
7543
|
+
access7 = createChronicleProjectAccess({ projectRoot });
|
|
6790
7544
|
} catch {
|
|
6791
7545
|
return [];
|
|
6792
7546
|
}
|
|
6793
7547
|
const provenance = [];
|
|
6794
7548
|
try {
|
|
6795
7549
|
const uniquePaths = [...new Set(filePaths)];
|
|
6796
|
-
const result = await
|
|
7550
|
+
const result = await access7.call("metrics", {
|
|
6797
7551
|
view: "files",
|
|
6798
7552
|
refresh: false,
|
|
6799
7553
|
files: {
|
|
@@ -6818,7 +7572,7 @@ async function findFileProvenance(projectRoot, filePaths) {
|
|
|
6818
7572
|
});
|
|
6819
7573
|
}
|
|
6820
7574
|
} finally {
|
|
6821
|
-
await
|
|
7575
|
+
await access7.close();
|
|
6822
7576
|
}
|
|
6823
7577
|
return provenance;
|
|
6824
7578
|
}
|
|
@@ -6915,6 +7669,7 @@ function resolveAutoReviewConfig(cfg, sessionConfig) {
|
|
|
6915
7669
|
provider: resolvedProvider,
|
|
6916
7670
|
model: resolvedModel,
|
|
6917
7671
|
fallbackModels,
|
|
7672
|
+
modelSelection: cfg.modelSelection === "random" ? "random" : "round-robin",
|
|
6918
7673
|
debounceMs: cfg.debounceMs ?? DEFAULT_DEBOUNCE_MS,
|
|
6919
7674
|
maxFilesPerBatch: cfg.maxFilesPerBatch ?? DEFAULT_MAX_FILES_PER_BATCH,
|
|
6920
7675
|
maxConcurrentReviews: cfg.maxConcurrentReviews ?? DEFAULT_MAX_CONCURRENT_REVIEWS,
|
|
@@ -7035,9 +7790,9 @@ async function snapshotChangedFiles(cwd) {
|
|
|
7035
7790
|
if (file.path.startsWith(".wrongstack/")) continue;
|
|
7036
7791
|
if (budget <= 0) break;
|
|
7037
7792
|
try {
|
|
7038
|
-
const
|
|
7039
|
-
if (!
|
|
7040
|
-
const content = await
|
|
7793
|
+
const stat8 = await fsp2.stat(path15.join(cwd, file.path));
|
|
7794
|
+
if (!stat8.isFile() || stat8.size > MAX_SNAPSHOT_FILE_BYTES) continue;
|
|
7795
|
+
const content = await fsp2.readFile(path15.join(cwd, file.path), "utf8");
|
|
7041
7796
|
budget -= content.length;
|
|
7042
7797
|
snapshots.push({
|
|
7043
7798
|
...file,
|
|
@@ -7059,7 +7814,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7059
7814
|
"",
|
|
7060
7815
|
"Continuous code review that fires on every change during a session.",
|
|
7061
7816
|
"Detects git-tracked file edits and dispatches review subagents",
|
|
7062
|
-
"with configurable provider/model/fallback.",
|
|
7817
|
+
"with configurable provider/model/fallback and profile selection.",
|
|
7063
7818
|
"",
|
|
7064
7819
|
"Reports are persisted and shown as passive notifications.",
|
|
7065
7820
|
"They never wake the leader or spawn mutating follow-up agents.",
|
|
@@ -7074,6 +7829,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7074
7829
|
" provider provider id for review agents",
|
|
7075
7830
|
" model model id for review agents",
|
|
7076
7831
|
" fallbackProfile named profile from config.fallbackProfiles",
|
|
7832
|
+
" modelSelection round-robin | random (default round-robin)",
|
|
7077
7833
|
" debounceMs debounce window (default 15000)",
|
|
7078
7834
|
" maxFilesPerBatch max files per review (default 15)",
|
|
7079
7835
|
" maxConcurrentReviews max parallel reviews (default 2)",
|
|
@@ -7100,7 +7856,8 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7100
7856
|
"",
|
|
7101
7857
|
` Provider: ${cfg.provider}`,
|
|
7102
7858
|
` Model: ${cfg.model}`,
|
|
7103
|
-
` Fallback:
|
|
7859
|
+
` Fallback / RR: ${cfg.fallbackModels.length > 0 ? cfg.fallbackModels.join(", ") : "(none)"}`,
|
|
7860
|
+
` Selection: ${cfg.modelSelection}`,
|
|
7104
7861
|
` Debounce: ${cfg.debounceMs} ms`,
|
|
7105
7862
|
` Max files: ${cfg.maxFilesPerBatch}`,
|
|
7106
7863
|
` Max parallel: ${cfg.maxConcurrentReviews}`,
|
|
@@ -7319,6 +8076,7 @@ function createAutoReviewPlugin() {
|
|
|
7319
8076
|
maxCascadeDepth: cfg.maxCascadeDepth
|
|
7320
8077
|
});
|
|
7321
8078
|
bundle.reviewFallbackModels = [...cfg.fallbackModels];
|
|
8079
|
+
bundle.reviewModelSelection = cfg.modelSelection;
|
|
7322
8080
|
const trackedChangedPaths = new Set(
|
|
7323
8081
|
snapshots.filter((file) => !file.path.startsWith(".wrongstack/")).map((file) => file.path)
|
|
7324
8082
|
);
|
|
@@ -7329,7 +8087,7 @@ function createAutoReviewPlugin() {
|
|
|
7329
8087
|
api.log.info(
|
|
7330
8088
|
`[auto-review] #${reviewCounter} emitting review_needed (${filesWithContent.length} files, provider=${cfg.provider} model=${cfg.model})`
|
|
7331
8089
|
);
|
|
7332
|
-
const emittedBundle = emitReviewIfChanged(api, bundle);
|
|
8090
|
+
const emittedBundle = await emitReviewIfChanged(api, bundle);
|
|
7333
8091
|
if (!emittedBundle) {
|
|
7334
8092
|
api.log.info(
|
|
7335
8093
|
`[auto-review] #${reviewCounter} skipped \u2014 file content already has a review in progress`
|
|
@@ -7362,89 +8120,94 @@ function createAutoReviewPlugin() {
|
|
|
7362
8120
|
api.onEvent("iteration.completed", async (payload) => {
|
|
7363
8121
|
await handleIterationCompleted?.(payload);
|
|
7364
8122
|
});
|
|
7365
|
-
api.onEvent("session.ended",
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
`[auto-review] session ended \u2014 handed ${handedOffFiles} pending mid-session file(s) to post-session review`
|
|
7372
|
-
);
|
|
7373
|
-
}
|
|
7374
|
-
await pendingReviewWork;
|
|
7375
|
-
try {
|
|
7376
|
-
const cfg = resolved;
|
|
7377
|
-
if (!cfg.enabled) return;
|
|
7378
|
-
const cwd = api.config.cwd ?? process.cwd();
|
|
7379
|
-
if (!await isGitRepo(cwd)) return;
|
|
7380
|
-
const allChanged = await getChangedFiles(cwd);
|
|
7381
|
-
const existing = [];
|
|
7382
|
-
for (const f of allChanged) {
|
|
7383
|
-
if (f.path.startsWith(".wrongstack/")) continue;
|
|
7384
|
-
try {
|
|
7385
|
-
await fsp.access(path14.join(cwd, f.path));
|
|
7386
|
-
existing.push(f);
|
|
7387
|
-
} catch {
|
|
7388
|
-
}
|
|
7389
|
-
}
|
|
7390
|
-
if (existing.length === 0) return;
|
|
7391
|
-
const toReview = existing.slice(0, cfg.maxFilesPerBatch);
|
|
7392
|
-
const filesWithContent = [];
|
|
7393
|
-
for (const f of toReview) {
|
|
7394
|
-
try {
|
|
7395
|
-
const absPath = path14.join(cwd, f.path);
|
|
7396
|
-
const content = await fsp.readFile(absPath, "utf8");
|
|
7397
|
-
filesWithContent.push({ path: f.path, status: f.status, content });
|
|
7398
|
-
} catch {
|
|
7399
|
-
}
|
|
7400
|
-
}
|
|
7401
|
-
if (filesWithContent.length === 0) return;
|
|
7402
|
-
if (inFlight.length >= cfg.maxConcurrentReviews) {
|
|
8123
|
+
api.onEvent("session.ended", (event) => {
|
|
8124
|
+
const work = (async () => {
|
|
8125
|
+
sessionEnded = true;
|
|
8126
|
+
const handedOffFiles = pendingFiles.size;
|
|
8127
|
+
cancelPendingReviewTimer();
|
|
8128
|
+
if (handedOffFiles > 0) {
|
|
7403
8129
|
api.log.info(
|
|
7404
|
-
`[auto-review] session
|
|
8130
|
+
`[auto-review] session ended \u2014 handed ${handedOffFiles} pending mid-session file(s) to post-session review`
|
|
7405
8131
|
);
|
|
7406
|
-
return;
|
|
7407
8132
|
}
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
8133
|
+
await pendingReviewWork;
|
|
8134
|
+
try {
|
|
8135
|
+
const cfg = resolved;
|
|
8136
|
+
if (!cfg.enabled) return;
|
|
8137
|
+
const cwd = api.config.cwd ?? process.cwd();
|
|
8138
|
+
if (!await isGitRepo(cwd)) return;
|
|
8139
|
+
const allChanged = await getChangedFiles(cwd);
|
|
8140
|
+
const existing = [];
|
|
8141
|
+
for (const f of allChanged) {
|
|
8142
|
+
if (f.path.startsWith(".wrongstack/")) continue;
|
|
8143
|
+
try {
|
|
8144
|
+
await fsp2.access(path15.join(cwd, f.path));
|
|
8145
|
+
existing.push(f);
|
|
8146
|
+
} catch {
|
|
8147
|
+
}
|
|
8148
|
+
}
|
|
8149
|
+
if (existing.length === 0) return;
|
|
8150
|
+
const toReview = existing.slice(0, cfg.maxFilesPerBatch);
|
|
8151
|
+
const filesWithContent = [];
|
|
8152
|
+
for (const f of toReview) {
|
|
8153
|
+
try {
|
|
8154
|
+
const absPath = path15.join(cwd, f.path);
|
|
8155
|
+
const content = await fsp2.readFile(absPath, "utf8");
|
|
8156
|
+
filesWithContent.push({ path: f.path, status: f.status, content });
|
|
8157
|
+
} catch {
|
|
8158
|
+
}
|
|
8159
|
+
}
|
|
8160
|
+
if (filesWithContent.length === 0) return;
|
|
8161
|
+
if (inFlight.length >= cfg.maxConcurrentReviews) {
|
|
8162
|
+
api.log.info(
|
|
8163
|
+
`[auto-review] session end \u2014 at max concurrent (${cfg.maxConcurrentReviews}), skipping final review`
|
|
8164
|
+
);
|
|
8165
|
+
return;
|
|
8166
|
+
}
|
|
8167
|
+
const bundle = await buildReviewContext({
|
|
8168
|
+
cwd,
|
|
8169
|
+
config: {
|
|
8170
|
+
enabled: true,
|
|
8171
|
+
provider: cfg.provider,
|
|
8172
|
+
model: cfg.model,
|
|
8173
|
+
maxFiles: cfg.maxFilesPerBatch,
|
|
8174
|
+
autoFix: "off",
|
|
8175
|
+
cascadeOn: "off",
|
|
8176
|
+
maxCascadeDepth: 0
|
|
8177
|
+
},
|
|
8178
|
+
files: filesWithContent,
|
|
8179
|
+
cascadeOn: cfg.cascadeOn,
|
|
8180
|
+
maxCascadeDepth: cfg.maxCascadeDepth
|
|
8181
|
+
});
|
|
8182
|
+
bundle.reviewFallbackModels = [...cfg.fallbackModels];
|
|
8183
|
+
bundle.reviewModelSelection = cfg.modelSelection;
|
|
8184
|
+
const trackedChangedPaths = new Set(existing.map((file) => file.path));
|
|
8185
|
+
bundle.allChangedFiles = bundle.allChangedFiles?.filter(
|
|
8186
|
+
(file) => trackedChangedPaths.has(file.path)
|
|
8187
|
+
);
|
|
8188
|
+
const emittedBundle = await emitReviewIfChanged(api, bundle);
|
|
8189
|
+
if (!emittedBundle) {
|
|
8190
|
+
api.log.info(
|
|
8191
|
+
"[auto-review] session end \u2014 unchanged files already have reviews in progress"
|
|
8192
|
+
);
|
|
8193
|
+
return;
|
|
8194
|
+
}
|
|
8195
|
+
const inflightEntry = {
|
|
8196
|
+
files: emittedBundle.files.map((file) => file.path),
|
|
8197
|
+
startedAt: Date.now(),
|
|
8198
|
+
subagentType: "review"
|
|
8199
|
+
};
|
|
8200
|
+
inFlight.push(inflightEntry);
|
|
8201
|
+
expireInFlight(inflightEntry);
|
|
7430
8202
|
api.log.info(
|
|
7431
|
-
|
|
8203
|
+
`[auto-review] session end \u2014 final review (${emittedBundle.files.length} files)`
|
|
7432
8204
|
);
|
|
7433
|
-
|
|
8205
|
+
} catch (err) {
|
|
8206
|
+
api.log.warn(`[auto-review] session.ended handler failed: ${toErrorMessage(err)}`);
|
|
7434
8207
|
}
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
subagentType: "review"
|
|
7439
|
-
};
|
|
7440
|
-
inFlight.push(inflightEntry);
|
|
7441
|
-
expireInFlight(inflightEntry);
|
|
7442
|
-
api.log.info(
|
|
7443
|
-
`[auto-review] session end \u2014 final review (${emittedBundle.files.length} files)`
|
|
7444
|
-
);
|
|
7445
|
-
} catch (err) {
|
|
7446
|
-
api.log.warn(`[auto-review] session.ended handler failed: ${toErrorMessage(err)}`);
|
|
7447
|
-
}
|
|
8208
|
+
})();
|
|
8209
|
+
event?.waitUntil?.(work);
|
|
8210
|
+
return work;
|
|
7448
8211
|
});
|
|
7449
8212
|
api.onPattern("chimera.review_complete", (_event, payload) => {
|
|
7450
8213
|
try {
|
|
@@ -7490,16 +8253,16 @@ function createAutoReviewPlugin() {
|
|
|
7490
8253
|
// src/plugins/chimera-plugin.ts
|
|
7491
8254
|
init_error();
|
|
7492
8255
|
import { spawn as spawn4 } from "node:child_process";
|
|
7493
|
-
import * as
|
|
8256
|
+
import * as fsp5 from "node:fs/promises";
|
|
7494
8257
|
import * as os5 from "node:os";
|
|
7495
|
-
import * as
|
|
8258
|
+
import * as path18 from "node:path";
|
|
7496
8259
|
|
|
7497
8260
|
// src/plugins/review-finding-commands.ts
|
|
7498
8261
|
init_review_finding_store();
|
|
7499
8262
|
|
|
7500
8263
|
// src/plugins/review-finding-parser.ts
|
|
7501
8264
|
init_review_finding_types();
|
|
7502
|
-
import { randomUUID as
|
|
8265
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
7503
8266
|
var SUGGEST_LINE = /^\s*(?:→|->|=>)\s*(.+)$/;
|
|
7504
8267
|
var DURATION_LINE = /^Duration:\s*(\d+)s\s*$/im;
|
|
7505
8268
|
function parseChimeraReviewReport(reportText, context = {}) {
|
|
@@ -7507,7 +8270,7 @@ function parseChimeraReviewReport(reportText, context = {}) {
|
|
|
7507
8270
|
return { findings: [], unparseableCount: 0 };
|
|
7508
8271
|
}
|
|
7509
8272
|
const findings = [];
|
|
7510
|
-
const reportId = context.reportId ??
|
|
8273
|
+
const reportId = context.reportId ?? randomUUID5();
|
|
7511
8274
|
let unparseableCount = 0;
|
|
7512
8275
|
let durationSeconds;
|
|
7513
8276
|
let currentSeverity = null;
|
|
@@ -7608,7 +8371,7 @@ function parseFindingSegment(segment, severity, context) {
|
|
|
7608
8371
|
const fullDesc = suggestions.length > 0 ? cleanDesc + "\n" + suggestions.map((s) => " \u2192 " + s).join("\n") : cleanDesc;
|
|
7609
8372
|
const suggestedFix = suggestions.length > 0 ? suggestions.join("\n") : void 0;
|
|
7610
8373
|
return {
|
|
7611
|
-
id:
|
|
8374
|
+
id: randomUUID5(),
|
|
7612
8375
|
fingerprint: computeFindingFingerprint(file ?? "", line ?? null, title),
|
|
7613
8376
|
severity,
|
|
7614
8377
|
source: normalizeFindingSource(context.reviewType),
|
|
@@ -7619,7 +8382,7 @@ function parseFindingSegment(segment, severity, context) {
|
|
|
7619
8382
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7620
8383
|
status: "active",
|
|
7621
8384
|
originReport: {
|
|
7622
|
-
reportId: context.reportId ??
|
|
8385
|
+
reportId: context.reportId ?? randomUUID5(),
|
|
7623
8386
|
sessionId: context.sessionId ?? "",
|
|
7624
8387
|
agentId: context.agentId ?? "",
|
|
7625
8388
|
reviewerModel: context.reviewerModel ?? ""
|
|
@@ -8273,7 +9036,10 @@ function buildChimeraCommand(getConfig, events) {
|
|
|
8273
9036
|
].join("\n")
|
|
8274
9037
|
};
|
|
8275
9038
|
}
|
|
8276
|
-
events.emit(
|
|
9039
|
+
events.emit(
|
|
9040
|
+
"chimera.set_autofix",
|
|
9041
|
+
{ mode }
|
|
9042
|
+
);
|
|
8277
9043
|
return {
|
|
8278
9044
|
message: `\u2705 Chimera autoFix set to "${mode}" for this session (config file unchanged).`
|
|
8279
9045
|
};
|
|
@@ -8312,8 +9078,8 @@ function buildReviewCommand(_getConfig) {
|
|
|
8312
9078
|
async run(args) {
|
|
8313
9079
|
const cwd = process.cwd();
|
|
8314
9080
|
const slug = projectSlug(cwd);
|
|
8315
|
-
const globalRoot =
|
|
8316
|
-
const projectDir =
|
|
9081
|
+
const globalRoot = path18.join(os5.homedir(), ".wrongstack");
|
|
9082
|
+
const projectDir = path18.join(globalRoot, "projects", slug);
|
|
8317
9083
|
const trimmed = (args ?? "").trim();
|
|
8318
9084
|
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
8319
9085
|
return {
|
|
@@ -8346,10 +9112,10 @@ function createChimeraPlugin() {
|
|
|
8346
9112
|
}
|
|
8347
9113
|
});
|
|
8348
9114
|
api.onPattern("chimera.review_needed", (_event, payload) => {
|
|
8349
|
-
recordStartedReview(api.events, payload);
|
|
9115
|
+
void recordStartedReview(api.events, payload).catch(() => void 0);
|
|
8350
9116
|
});
|
|
8351
9117
|
api.onPattern("chimera.review_complete", (_event, payload) => {
|
|
8352
|
-
recordCompletedReview(api.events, payload);
|
|
9118
|
+
void recordCompletedReview(api.events, payload).catch(() => void 0);
|
|
8353
9119
|
});
|
|
8354
9120
|
if (!resolved.enabled) {
|
|
8355
9121
|
api.log.info("[chimera] disabled by config");
|
|
@@ -8360,64 +9126,70 @@ function createChimeraPlugin() {
|
|
|
8360
9126
|
);
|
|
8361
9127
|
api.slashCommands.register(buildChimeraCommand(() => resolved, api.events));
|
|
8362
9128
|
api.slashCommands.register(buildReviewCommand(() => resolved));
|
|
8363
|
-
api.onEvent("session.ended",
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
const allChanged = await getChangedFiles2(cwd);
|
|
8373
|
-
const existing = [];
|
|
8374
|
-
for (const f of allChanged) {
|
|
8375
|
-
if (f.path.startsWith(".wrongstack/")) continue;
|
|
8376
|
-
try {
|
|
8377
|
-
await fsp4.access(path17.join(cwd, f.path));
|
|
8378
|
-
existing.push(f);
|
|
8379
|
-
} catch {
|
|
9129
|
+
api.onEvent("session.ended", (event) => {
|
|
9130
|
+
const work = (async () => {
|
|
9131
|
+
try {
|
|
9132
|
+
const cfg = resolved;
|
|
9133
|
+
if (!cfg.enabled) return;
|
|
9134
|
+
const cwd = api.config.cwd ?? process.cwd();
|
|
9135
|
+
if (!await isGitRepo2(cwd)) {
|
|
9136
|
+
api.log.info("[chimera] skipped \u2014 not a git repo");
|
|
9137
|
+
return;
|
|
8380
9138
|
}
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
const filesWithContent = [];
|
|
8391
|
-
for (const f of toReview) {
|
|
8392
|
-
try {
|
|
8393
|
-
const absPath = path17.join(cwd, f.path);
|
|
8394
|
-
const content = await fsp4.readFile(absPath, "utf8");
|
|
8395
|
-
filesWithContent.push({ path: f.path, status: f.status, content });
|
|
8396
|
-
} catch {
|
|
9139
|
+
const allChanged = await getChangedFiles2(cwd);
|
|
9140
|
+
const existing = [];
|
|
9141
|
+
for (const f of allChanged) {
|
|
9142
|
+
if (f.path.startsWith(".wrongstack/")) continue;
|
|
9143
|
+
try {
|
|
9144
|
+
await fsp5.access(path18.join(cwd, f.path));
|
|
9145
|
+
existing.push(f);
|
|
9146
|
+
} catch {
|
|
9147
|
+
}
|
|
8397
9148
|
}
|
|
9149
|
+
if (existing.length === 0) {
|
|
9150
|
+
api.log.info("[chimera] no changed files to review");
|
|
9151
|
+
return;
|
|
9152
|
+
}
|
|
9153
|
+
const toReview = existing.slice(0, cfg.maxFiles);
|
|
9154
|
+
if (existing.length > cfg.maxFiles) {
|
|
9155
|
+
api.log.info(
|
|
9156
|
+
`[chimera] capping review at ${cfg.maxFiles} of ${existing.length} files`
|
|
9157
|
+
);
|
|
9158
|
+
}
|
|
9159
|
+
const filesWithContent = [];
|
|
9160
|
+
for (const f of toReview) {
|
|
9161
|
+
try {
|
|
9162
|
+
const absPath = path18.join(cwd, f.path);
|
|
9163
|
+
const content = await fsp5.readFile(absPath, "utf8");
|
|
9164
|
+
filesWithContent.push({ path: f.path, status: f.status, content });
|
|
9165
|
+
} catch {
|
|
9166
|
+
}
|
|
9167
|
+
}
|
|
9168
|
+
if (filesWithContent.length === 0) {
|
|
9169
|
+
api.log.info("[chimera] could not read changed files");
|
|
9170
|
+
return;
|
|
9171
|
+
}
|
|
9172
|
+
const bundle = await buildReviewContext({
|
|
9173
|
+
cwd,
|
|
9174
|
+
config: cfg,
|
|
9175
|
+
files: filesWithContent,
|
|
9176
|
+
cascadeOn: cfg.cascadeOn,
|
|
9177
|
+
maxCascadeDepth: cfg.maxCascadeDepth
|
|
9178
|
+
});
|
|
9179
|
+
const emittedBundle = await emitReviewIfChanged(api, bundle);
|
|
9180
|
+
if (!emittedBundle) {
|
|
9181
|
+
api.log.info("[chimera] skipped \u2014 changed files already have reviews in progress");
|
|
9182
|
+
return;
|
|
9183
|
+
}
|
|
9184
|
+
api.log.info(
|
|
9185
|
+
`[chimera] emitted review_needed event (${emittedBundle.files.length} files)`
|
|
9186
|
+
);
|
|
9187
|
+
} catch (err) {
|
|
9188
|
+
api.log.warn(`[chimera] session.ended handler failed: ${toErrorMessage(err)}`);
|
|
8398
9189
|
}
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
}
|
|
8403
|
-
const bundle = await buildReviewContext({
|
|
8404
|
-
cwd,
|
|
8405
|
-
config: cfg,
|
|
8406
|
-
files: filesWithContent,
|
|
8407
|
-
cascadeOn: cfg.cascadeOn,
|
|
8408
|
-
maxCascadeDepth: cfg.maxCascadeDepth
|
|
8409
|
-
});
|
|
8410
|
-
const emittedBundle = emitReviewIfChanged(api, bundle);
|
|
8411
|
-
if (!emittedBundle) {
|
|
8412
|
-
api.log.info("[chimera] skipped \u2014 changed files already have reviews in progress");
|
|
8413
|
-
return;
|
|
8414
|
-
}
|
|
8415
|
-
api.log.info(
|
|
8416
|
-
`[chimera] emitted review_needed event (${emittedBundle.files.length} files)`
|
|
8417
|
-
);
|
|
8418
|
-
} catch (err) {
|
|
8419
|
-
api.log.warn(`[chimera] session.ended handler failed: ${toErrorMessage(err)}`);
|
|
8420
|
-
}
|
|
9190
|
+
})();
|
|
9191
|
+
event?.waitUntil?.(work);
|
|
9192
|
+
return work;
|
|
8421
9193
|
});
|
|
8422
9194
|
},
|
|
8423
9195
|
teardown(api) {
|
|
@@ -8432,17 +9204,17 @@ function createChimeraPlugin() {
|
|
|
8432
9204
|
}
|
|
8433
9205
|
|
|
8434
9206
|
// src/plugins/prompts-plugin.ts
|
|
8435
|
-
import * as
|
|
8436
|
-
import * as
|
|
9207
|
+
import * as fs13 from "node:fs/promises";
|
|
9208
|
+
import * as path21 from "node:path";
|
|
8437
9209
|
|
|
8438
9210
|
// src/execution/prompt-loader.ts
|
|
8439
|
-
import * as
|
|
8440
|
-
import * as
|
|
9211
|
+
import * as fs11 from "node:fs/promises";
|
|
9212
|
+
import * as path20 from "node:path";
|
|
8441
9213
|
|
|
8442
9214
|
// src/storage/prompt-store.ts
|
|
8443
9215
|
init_atomic_write();
|
|
8444
|
-
import * as
|
|
8445
|
-
import * as
|
|
9216
|
+
import * as fs10 from "node:fs/promises";
|
|
9217
|
+
import * as path19 from "node:path";
|
|
8446
9218
|
var SCHEMA_VERSION3 = 2;
|
|
8447
9219
|
function migratePromptEntry(raw) {
|
|
8448
9220
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -8503,12 +9275,12 @@ var DefaultPromptStore = class {
|
|
|
8503
9275
|
await ensureDir(this.dir);
|
|
8504
9276
|
const entries = [];
|
|
8505
9277
|
try {
|
|
8506
|
-
const files = await
|
|
9278
|
+
const files = await fs10.readdir(this.dir);
|
|
8507
9279
|
for (const file of files) {
|
|
8508
9280
|
if (!file.endsWith(".json")) continue;
|
|
8509
9281
|
try {
|
|
8510
9282
|
const raw = JSON.parse(
|
|
8511
|
-
await
|
|
9283
|
+
await fs10.readFile(path19.join(this.dir, file), "utf8")
|
|
8512
9284
|
);
|
|
8513
9285
|
const migrated = migratePromptEntry(raw.entry);
|
|
8514
9286
|
if (migrated) entries.push(migrated);
|
|
@@ -8522,9 +9294,9 @@ var DefaultPromptStore = class {
|
|
|
8522
9294
|
);
|
|
8523
9295
|
}
|
|
8524
9296
|
async get(id) {
|
|
8525
|
-
const file =
|
|
9297
|
+
const file = path19.join(this.dir, `${id}.json`);
|
|
8526
9298
|
try {
|
|
8527
|
-
const raw = JSON.parse(await
|
|
9299
|
+
const raw = JSON.parse(await fs10.readFile(file, "utf8"));
|
|
8528
9300
|
return migratePromptEntry(raw.entry);
|
|
8529
9301
|
} catch {
|
|
8530
9302
|
return null;
|
|
@@ -8532,14 +9304,14 @@ var DefaultPromptStore = class {
|
|
|
8532
9304
|
}
|
|
8533
9305
|
async save(entry) {
|
|
8534
9306
|
await ensureDir(this.dir);
|
|
8535
|
-
const file =
|
|
9307
|
+
const file = path19.join(this.dir, `${entry.id}.json`);
|
|
8536
9308
|
const raw = { version: SCHEMA_VERSION3, entry };
|
|
8537
9309
|
await atomicWrite(file, JSON.stringify(raw, null, 2));
|
|
8538
9310
|
}
|
|
8539
9311
|
async delete(id) {
|
|
8540
|
-
const file =
|
|
9312
|
+
const file = path19.join(this.dir, `${id}.json`);
|
|
8541
9313
|
try {
|
|
8542
|
-
await
|
|
9314
|
+
await fs10.unlink(file);
|
|
8543
9315
|
return true;
|
|
8544
9316
|
} catch {
|
|
8545
9317
|
return false;
|
|
@@ -8626,7 +9398,7 @@ var DefaultPromptLoader = class {
|
|
|
8626
9398
|
constructor(opts) {
|
|
8627
9399
|
this.projectStore = typeof opts.paths.inProjectPrompts === "string" ? new DefaultPromptStore(opts.paths.inProjectPrompts) : void 0;
|
|
8628
9400
|
this.userStore = typeof opts.paths.globalPrompts === "string" ? new DefaultPromptStore(opts.paths.globalPrompts) : void 0;
|
|
8629
|
-
this.builtinDir = opts.bundledDir ?
|
|
9401
|
+
this.builtinDir = opts.bundledDir ? path20.join(opts.bundledDir, "prompts") : void 0;
|
|
8630
9402
|
}
|
|
8631
9403
|
async list() {
|
|
8632
9404
|
if (this.cache) return this.cache;
|
|
@@ -8739,7 +9511,7 @@ var DefaultPromptLoader = class {
|
|
|
8739
9511
|
const files = await walkJson(dir);
|
|
8740
9512
|
for (const file of files) {
|
|
8741
9513
|
try {
|
|
8742
|
-
const parsed = JSON.parse(await
|
|
9514
|
+
const parsed = JSON.parse(await fs11.readFile(file, "utf8"));
|
|
8743
9515
|
const migrated = migratePromptEntry(parsed);
|
|
8744
9516
|
if (migrated) out.push({ ...migrated, source: "builtin" });
|
|
8745
9517
|
} catch {
|
|
@@ -8753,12 +9525,12 @@ async function walkJson(dir) {
|
|
|
8753
9525
|
const out = [];
|
|
8754
9526
|
let entries;
|
|
8755
9527
|
try {
|
|
8756
|
-
entries = await
|
|
9528
|
+
entries = await fs11.readdir(dir, { withFileTypes: true });
|
|
8757
9529
|
} catch {
|
|
8758
9530
|
return out;
|
|
8759
9531
|
}
|
|
8760
9532
|
for (const e of entries) {
|
|
8761
|
-
const full =
|
|
9533
|
+
const full = path20.join(dir, e.name);
|
|
8762
9534
|
if (e.isDirectory()) {
|
|
8763
9535
|
out.push(...await walkJson(full));
|
|
8764
9536
|
} else if (e.name.endsWith(".json") && e.name !== "index.json" && e.name !== "schema.json") {
|
|
@@ -8833,7 +9605,7 @@ function escapeRegExp(s) {
|
|
|
8833
9605
|
|
|
8834
9606
|
// src/storage/prompt-usage-store.ts
|
|
8835
9607
|
init_atomic_write();
|
|
8836
|
-
import * as
|
|
9608
|
+
import * as fs12 from "node:fs/promises";
|
|
8837
9609
|
var PromptUsageStore = class {
|
|
8838
9610
|
constructor(file) {
|
|
8839
9611
|
this.file = file;
|
|
@@ -8853,7 +9625,7 @@ var PromptUsageStore = class {
|
|
|
8853
9625
|
return this.cachedUsage;
|
|
8854
9626
|
}
|
|
8855
9627
|
try {
|
|
8856
|
-
const raw = JSON.parse(await
|
|
9628
|
+
const raw = JSON.parse(await fs12.readFile(this.file, "utf8"));
|
|
8857
9629
|
if (raw && typeof raw === "object" && raw.usage && typeof raw.usage === "object") {
|
|
8858
9630
|
this.cachedUsage = raw.usage;
|
|
8859
9631
|
this.cachedSignature = signature;
|
|
@@ -8933,8 +9705,8 @@ var PromptUsageStore = class {
|
|
|
8933
9705
|
}
|
|
8934
9706
|
async fileSignature() {
|
|
8935
9707
|
try {
|
|
8936
|
-
const
|
|
8937
|
-
return { size:
|
|
9708
|
+
const stat8 = await fs12.stat(this.file);
|
|
9709
|
+
return { size: stat8.size, mtimeMs: stat8.mtimeMs, ctimeMs: stat8.ctimeMs };
|
|
8938
9710
|
} catch {
|
|
8939
9711
|
return null;
|
|
8940
9712
|
}
|
|
@@ -8958,7 +9730,7 @@ function createPromptsPlugin(opts) {
|
|
|
8958
9730
|
version: "1.0.0",
|
|
8959
9731
|
description: "Prompt library with 100+ builtin prompts, search, and AI authoring",
|
|
8960
9732
|
apiVersion: "^0.1",
|
|
8961
|
-
capabilities: { slashCommands: true },
|
|
9733
|
+
capabilities: { slashCommands: true, llm: true },
|
|
8962
9734
|
defaultConfig: {},
|
|
8963
9735
|
setup(api) {
|
|
8964
9736
|
const rawConfig = api.config;
|
|
@@ -8969,7 +9741,7 @@ function createPromptsPlugin(opts) {
|
|
|
8969
9741
|
bundledDir: rawConfig["bundledPromptsDir"]
|
|
8970
9742
|
}) : null);
|
|
8971
9743
|
usage = opts?.usage ?? (paths ? new PromptUsageStore(paths.promptUsage) : null);
|
|
8972
|
-
api.slashCommands.register(buildPromptsCommand(() => store, () => loader));
|
|
9744
|
+
api.slashCommands.register(buildPromptsCommand(() => store, () => loader, () => api.llm));
|
|
8973
9745
|
api.slashCommands.register(buildPromptSearchCommand(() => loader, () => usage));
|
|
8974
9746
|
api.slashCommands.register(buildPromptGenCommand(() => loader));
|
|
8975
9747
|
api.log.info("[prompts] loaded \u2014 /prompts, /prompt, /prompt-gen available");
|
|
@@ -8985,7 +9757,7 @@ function createPromptsPlugin(opts) {
|
|
|
8985
9757
|
}
|
|
8986
9758
|
};
|
|
8987
9759
|
}
|
|
8988
|
-
function buildPromptsCommand(getStore, getLoader) {
|
|
9760
|
+
function buildPromptsCommand(getStore, getLoader, getLlm) {
|
|
8989
9761
|
return {
|
|
8990
9762
|
name: "prompts",
|
|
8991
9763
|
description: "Manage your prompt library: /prompts [list|view|add|edit|delete|favorite|extend]",
|
|
@@ -9080,16 +9852,25 @@ ${lines.join("\n")}
|
|
|
9080
9852
|
const matches2 = await store.find(parsed.title);
|
|
9081
9853
|
if (matches2.length === 0) return { message: `No prompt matching "${parsed.title}".` };
|
|
9082
9854
|
const exact = matches2.find((m) => m.title.toLowerCase() === parsed.title?.toLowerCase()) ?? expectDefined(matches2[0]);
|
|
9083
|
-
const
|
|
9084
|
-
if (!
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9855
|
+
const llm = getLlm();
|
|
9856
|
+
if (!llm) return { message: "LLM not available. Configure a provider first." };
|
|
9857
|
+
let enhanced;
|
|
9858
|
+
try {
|
|
9859
|
+
enhanced = await llm.complete(
|
|
9860
|
+
renderInstructionTemplate(readBundledInstructionText("llm/prompt-extend.md"), {
|
|
9861
|
+
existingPrompt: exact.content,
|
|
9862
|
+
additionalInstructions: parsed.content
|
|
9863
|
+
}),
|
|
9864
|
+
{
|
|
9865
|
+
system: "You improve reusable prompts while preserving their intent and variables.",
|
|
9866
|
+
role: "prompt-refiner",
|
|
9867
|
+
maxTokens: 2048
|
|
9868
|
+
}
|
|
9869
|
+
);
|
|
9870
|
+
} catch {
|
|
9871
|
+
return { message: "LLM enhancement failed. The saved prompt was not changed." };
|
|
9872
|
+
}
|
|
9873
|
+
exact.content = enhanced.text.trim();
|
|
9093
9874
|
exact.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9094
9875
|
await store.save(exact);
|
|
9095
9876
|
getLoader()?.invalidateCache();
|
|
@@ -9111,7 +9892,7 @@ ${exact.content}` };
|
|
|
9111
9892
|
2
|
|
9112
9893
|
);
|
|
9113
9894
|
try {
|
|
9114
|
-
await
|
|
9895
|
+
await fs13.writeFile(target, payload, "utf8");
|
|
9115
9896
|
return { message: `Exported ${own.length} prompt(s) \u2192 ${target}` };
|
|
9116
9897
|
} catch (err) {
|
|
9117
9898
|
return { message: `Export failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -9123,7 +9904,7 @@ ${exact.content}` };
|
|
|
9123
9904
|
const src = resolveIoPath(restJoined, ctx);
|
|
9124
9905
|
let raw;
|
|
9125
9906
|
try {
|
|
9126
|
-
raw = JSON.parse(await
|
|
9907
|
+
raw = JSON.parse(await fs13.readFile(src, "utf8"));
|
|
9127
9908
|
} catch (err) {
|
|
9128
9909
|
return { message: `Import failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
9129
9910
|
}
|
|
@@ -9329,9 +10110,9 @@ ${dim(`slug: ${entry.slug} | category: ${entry.category} | source: ${entry.sourc
|
|
|
9329
10110
|
}
|
|
9330
10111
|
function resolveIoPath(p, ctx) {
|
|
9331
10112
|
const cleaned = p.trim().replace(/^["']|["']$/g, "");
|
|
9332
|
-
if (
|
|
10113
|
+
if (path21.isAbsolute(cleaned)) return cleaned;
|
|
9333
10114
|
const base = ctx.projectRoot ?? process.cwd();
|
|
9334
|
-
return
|
|
10115
|
+
return path21.resolve(base, cleaned);
|
|
9335
10116
|
}
|
|
9336
10117
|
function sourceGlyph(e) {
|
|
9337
10118
|
return e.source === "project" ? "\u{1F4C1}" : e.source === "user" ? "\u{1F464}" : e.source === "synced" ? "\u2601" : "\u{1F4E6}";
|
|
@@ -9455,8 +10236,8 @@ async function integrateFindings(payload, projectDir, reportId) {
|
|
|
9455
10236
|
init_atomic_write();
|
|
9456
10237
|
init_review_finding_store();
|
|
9457
10238
|
init_review_report_store();
|
|
9458
|
-
import * as
|
|
9459
|
-
import * as
|
|
10239
|
+
import * as fsp6 from "node:fs/promises";
|
|
10240
|
+
import * as path22 from "node:path";
|
|
9460
10241
|
var REVIEW_STORE_COMPACTION_THRESHOLD_BYTES = 8 * 1024 * 1024;
|
|
9461
10242
|
var REVIEW_STORE_COMPACTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
9462
10243
|
var REVIEW_STORE_MAINTENANCE_FILE = ".review-store-maintenance.json";
|
|
@@ -9464,7 +10245,7 @@ async function maybeCompactReviewStores(projectDir, opts = {}) {
|
|
|
9464
10245
|
const thresholdBytes = opts.thresholdBytes ?? REVIEW_STORE_COMPACTION_THRESHOLD_BYTES;
|
|
9465
10246
|
const intervalMs = opts.intervalMs ?? REVIEW_STORE_COMPACTION_INTERVAL_MS;
|
|
9466
10247
|
const now = opts.now ?? Date.now();
|
|
9467
|
-
const maintenancePath =
|
|
10248
|
+
const maintenancePath = path22.join(projectDir, REVIEW_STORE_MAINTENANCE_FILE);
|
|
9468
10249
|
return withFileLock(maintenancePath, async () => {
|
|
9469
10250
|
const totalBytes = await fileSize(resolveReportStorePath(projectDir)) + await fileSize(resolveFindingStorePath(projectDir));
|
|
9470
10251
|
if (totalBytes < thresholdBytes) {
|
|
@@ -9504,7 +10285,7 @@ function emptyResult(reason, totalBytes) {
|
|
|
9504
10285
|
}
|
|
9505
10286
|
async function fileSize(filePath) {
|
|
9506
10287
|
try {
|
|
9507
|
-
return (await
|
|
10288
|
+
return (await fsp6.stat(filePath)).size;
|
|
9508
10289
|
} catch (error) {
|
|
9509
10290
|
if (error.code === "ENOENT") return 0;
|
|
9510
10291
|
throw error;
|
|
@@ -9512,7 +10293,7 @@ async function fileSize(filePath) {
|
|
|
9512
10293
|
}
|
|
9513
10294
|
async function readMaintenanceState(filePath) {
|
|
9514
10295
|
try {
|
|
9515
|
-
const parsed = JSON.parse(await
|
|
10296
|
+
const parsed = JSON.parse(await fsp6.readFile(filePath, "utf8"));
|
|
9516
10297
|
return typeof parsed.compactedAt === "string" && Number.isFinite(parsed.totalBytesBefore) ? parsed : null;
|
|
9517
10298
|
} catch (error) {
|
|
9518
10299
|
if (error.code === "ENOENT" || error instanceof SyntaxError) {
|
|
@@ -9524,7 +10305,7 @@ async function readMaintenanceState(filePath) {
|
|
|
9524
10305
|
|
|
9525
10306
|
// src/plugins/skills-plugin.ts
|
|
9526
10307
|
import * as os7 from "node:os";
|
|
9527
|
-
import * as
|
|
10308
|
+
import * as path27 from "node:path";
|
|
9528
10309
|
|
|
9529
10310
|
// src/skills/foreign-sources.ts
|
|
9530
10311
|
var FOREIGN_SKILL_TOOLS = [
|
|
@@ -9706,8 +10487,8 @@ function numField(rec, key) {
|
|
|
9706
10487
|
// src/skills/skill-generator.ts
|
|
9707
10488
|
init_errors();
|
|
9708
10489
|
import { spawn as spawn5 } from "node:child_process";
|
|
9709
|
-
import * as
|
|
9710
|
-
import * as
|
|
10490
|
+
import * as fs14 from "node:fs/promises";
|
|
10491
|
+
import * as path23 from "node:path";
|
|
9711
10492
|
async function validateSkillNameAvailable(name, loader) {
|
|
9712
10493
|
const formatViolations = validateSkillName(name);
|
|
9713
10494
|
const conflicts = loader ? (await loader.listEntries()).filter((e) => e.name === name) : [];
|
|
@@ -9812,7 +10593,12 @@ async function openInEditor(filePath, env = process.env) {
|
|
|
9812
10593
|
const child = spawn5(parts[0], [...parts.slice(1), filePath], {
|
|
9813
10594
|
stdio: "ignore",
|
|
9814
10595
|
detached: true,
|
|
9815
|
-
shell
|
|
10596
|
+
shell,
|
|
10597
|
+
// `shell` routes through cmd.exe on win32, which flashes a console window
|
|
10598
|
+
// before the editor appears. A GUI editor is unaffected by the flag; the
|
|
10599
|
+
// shell wrapper is what it suppresses. Repo convention — see
|
|
10600
|
+
// `core/tests/architecture/spawn-convention.test.ts`.
|
|
10601
|
+
windowsHide: true
|
|
9816
10602
|
});
|
|
9817
10603
|
child.unref();
|
|
9818
10604
|
}
|
|
@@ -9826,11 +10612,11 @@ async function writeSkeletonSkill(skillsDir, body, opts = {}) {
|
|
|
9826
10612
|
subsystem: "general"
|
|
9827
10613
|
});
|
|
9828
10614
|
}
|
|
9829
|
-
const skillDir =
|
|
9830
|
-
const skillFile =
|
|
10615
|
+
const skillDir = path23.join(skillsDir, name);
|
|
10616
|
+
const skillFile = path23.join(skillDir, "SKILL.md");
|
|
9831
10617
|
if (!opts.overwrite) {
|
|
9832
10618
|
try {
|
|
9833
|
-
await
|
|
10619
|
+
await fs14.access(skillFile);
|
|
9834
10620
|
throw new WrongStackError({
|
|
9835
10621
|
message: `A skill already exists at ${skillFile}. Use --force to overwrite.`,
|
|
9836
10622
|
code: ERROR_CODES.VALIDATION_ERROR,
|
|
@@ -9841,8 +10627,8 @@ async function writeSkeletonSkill(skillsDir, body, opts = {}) {
|
|
|
9841
10627
|
if (err instanceof WrongStackError) throw err;
|
|
9842
10628
|
}
|
|
9843
10629
|
}
|
|
9844
|
-
await
|
|
9845
|
-
await
|
|
10630
|
+
await fs14.mkdir(skillDir, { recursive: true });
|
|
10631
|
+
await fs14.writeFile(skillFile, body, "utf8");
|
|
9846
10632
|
return skillFile;
|
|
9847
10633
|
}
|
|
9848
10634
|
function bodyLineAdvisory(body) {
|
|
@@ -9885,15 +10671,15 @@ function defaultEditor() {
|
|
|
9885
10671
|
// src/skills/skill-installer.ts
|
|
9886
10672
|
init_errors();
|
|
9887
10673
|
init_error();
|
|
9888
|
-
import * as
|
|
9889
|
-
import * as
|
|
10674
|
+
import * as fs17 from "node:fs/promises";
|
|
10675
|
+
import * as path26 from "node:path";
|
|
9890
10676
|
|
|
9891
10677
|
// src/skills/github-fetcher.ts
|
|
9892
10678
|
init_errors();
|
|
9893
10679
|
import { createWriteStream } from "node:fs";
|
|
9894
|
-
import * as
|
|
10680
|
+
import * as fs15 from "node:fs/promises";
|
|
9895
10681
|
import * as os6 from "node:os";
|
|
9896
|
-
import * as
|
|
10682
|
+
import * as path24 from "node:path";
|
|
9897
10683
|
import { Readable } from "node:stream";
|
|
9898
10684
|
import { pipeline } from "node:stream/promises";
|
|
9899
10685
|
import { createGunzip } from "node:zlib";
|
|
@@ -10035,7 +10821,7 @@ async function downloadGitHubTarball(parsed) {
|
|
|
10035
10821
|
}
|
|
10036
10822
|
});
|
|
10037
10823
|
}
|
|
10038
|
-
const tempDir = await
|
|
10824
|
+
const tempDir = await fs15.mkdtemp(path24.join(os6.tmpdir(), "wskill-"));
|
|
10039
10825
|
try {
|
|
10040
10826
|
if (!response.body) {
|
|
10041
10827
|
throw new WrongStackError({
|
|
@@ -10046,17 +10832,17 @@ async function downloadGitHubTarball(parsed) {
|
|
|
10046
10832
|
});
|
|
10047
10833
|
}
|
|
10048
10834
|
const nodeStream = Readable.fromWeb(response.body);
|
|
10049
|
-
const tarPath =
|
|
10835
|
+
const tarPath = path24.join(tempDir, ".wrongstack-download.tar");
|
|
10050
10836
|
await writeBoundedGzipStream(nodeStream, tarPath, SKILL_LIMITS.MAX_UNCOMPRESSED_TARBALL_SIZE);
|
|
10051
|
-
const tarBuf = await
|
|
10837
|
+
const tarBuf = await fs15.readFile(tarPath);
|
|
10052
10838
|
try {
|
|
10053
10839
|
await extractTar(tarBuf, tempDir);
|
|
10054
10840
|
} finally {
|
|
10055
|
-
await
|
|
10841
|
+
await fs15.rm(tarPath, { force: true });
|
|
10056
10842
|
}
|
|
10057
10843
|
return { tempDir };
|
|
10058
10844
|
} catch (err) {
|
|
10059
|
-
await
|
|
10845
|
+
await fs15.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
10060
10846
|
});
|
|
10061
10847
|
throw err;
|
|
10062
10848
|
}
|
|
@@ -10111,25 +10897,25 @@ async function extractTar(buf, destDir) {
|
|
|
10111
10897
|
const fullPath = prefix ? `${prefix}/${name}` : name;
|
|
10112
10898
|
const relPath = stripTopDir(fullPath);
|
|
10113
10899
|
if (relPath && relPath !== "." && relPath !== "..") {
|
|
10114
|
-
const destPath =
|
|
10115
|
-
const resolvedDest =
|
|
10116
|
-
const resolvedRoot =
|
|
10117
|
-
if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot +
|
|
10900
|
+
const destPath = path24.join(destDir, relPath);
|
|
10901
|
+
const resolvedDest = path24.resolve(destPath);
|
|
10902
|
+
const resolvedRoot = path24.resolve(destDir);
|
|
10903
|
+
if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot + path24.sep)) {
|
|
10118
10904
|
offset += 512 + Math.ceil(size / 512) * 512;
|
|
10119
10905
|
continue;
|
|
10120
10906
|
}
|
|
10121
10907
|
if (typeflag === 53 || typeflag === 0) {
|
|
10122
10908
|
if (relPath.endsWith("/") || typeflag === 53) {
|
|
10123
|
-
await
|
|
10909
|
+
await fs15.mkdir(destPath, { recursive: true });
|
|
10124
10910
|
}
|
|
10125
10911
|
}
|
|
10126
10912
|
if ((typeflag === 48 || typeflag === 0 || typeflag === 0) && size > 0) {
|
|
10127
|
-
const dir =
|
|
10128
|
-
await
|
|
10913
|
+
const dir = path24.dirname(destPath);
|
|
10914
|
+
await fs15.mkdir(dir, { recursive: true });
|
|
10129
10915
|
const dataStart = offset + 512;
|
|
10130
10916
|
const dataEnd = dataStart + size;
|
|
10131
10917
|
if (dataEnd > buf.length) break;
|
|
10132
|
-
await
|
|
10918
|
+
await fs15.writeFile(destPath, buf.subarray(dataStart, dataEnd));
|
|
10133
10919
|
}
|
|
10134
10920
|
}
|
|
10135
10921
|
offset += 512 + Math.ceil(size / 512) * 512;
|
|
@@ -10138,8 +10924,8 @@ async function extractTar(buf, destDir) {
|
|
|
10138
10924
|
|
|
10139
10925
|
// src/skills/manifest-store.ts
|
|
10140
10926
|
init_atomic_write();
|
|
10141
|
-
import * as
|
|
10142
|
-
import * as
|
|
10927
|
+
import * as fs16 from "node:fs/promises";
|
|
10928
|
+
import * as path25 from "node:path";
|
|
10143
10929
|
var SkillManifestStore = class {
|
|
10144
10930
|
manifestPath;
|
|
10145
10931
|
cache;
|
|
@@ -10149,7 +10935,7 @@ var SkillManifestStore = class {
|
|
|
10149
10935
|
async read() {
|
|
10150
10936
|
if (this.cache) return this.cache;
|
|
10151
10937
|
try {
|
|
10152
|
-
const raw = await
|
|
10938
|
+
const raw = await fs16.readFile(this.manifestPath, "utf8");
|
|
10153
10939
|
const data = JSON.parse(raw);
|
|
10154
10940
|
if (!Array.isArray(data.skills)) {
|
|
10155
10941
|
this.cache = { skills: [] };
|
|
@@ -10162,8 +10948,8 @@ var SkillManifestStore = class {
|
|
|
10162
10948
|
return this.cache;
|
|
10163
10949
|
}
|
|
10164
10950
|
async write(data) {
|
|
10165
|
-
const dir =
|
|
10166
|
-
await
|
|
10951
|
+
const dir = path25.dirname(this.manifestPath);
|
|
10952
|
+
await fs16.mkdir(dir, { recursive: true });
|
|
10167
10953
|
await atomicWrite(this.manifestPath, JSON.stringify(data, null, 2) + "\n");
|
|
10168
10954
|
this.cache = data;
|
|
10169
10955
|
}
|
|
@@ -10201,8 +10987,8 @@ var SkillManifestStore = class {
|
|
|
10201
10987
|
|
|
10202
10988
|
// src/skills/skill-installer.ts
|
|
10203
10989
|
function isInside(resolved, destDir) {
|
|
10204
|
-
const root =
|
|
10205
|
-
return resolved === root || resolved.startsWith(root +
|
|
10990
|
+
const root = path26.resolve(destDir);
|
|
10991
|
+
return resolved === root || resolved.startsWith(root + path26.sep);
|
|
10206
10992
|
}
|
|
10207
10993
|
var MAX_SKILL_FILE_SIZE = SKILL_LIMITS.MAX_SKILL_FILE_SIZE;
|
|
10208
10994
|
var SkillInstaller = class {
|
|
@@ -10257,13 +11043,13 @@ var SkillInstaller = class {
|
|
|
10257
11043
|
this.opts.log?.(`Overwriting existing skill "${skill.name}" (${scope})...`);
|
|
10258
11044
|
await this.removeSkillFiles(skill.name, scope);
|
|
10259
11045
|
}
|
|
10260
|
-
const destDir =
|
|
10261
|
-
await
|
|
11046
|
+
const destDir = path26.join(targetDir, skill.name);
|
|
11047
|
+
await fs17.mkdir(destDir, { recursive: true });
|
|
10262
11048
|
const copiedFiles = [];
|
|
10263
11049
|
for (const file of skill.files) {
|
|
10264
|
-
const srcPath =
|
|
10265
|
-
const destPath =
|
|
10266
|
-
const resolved2 =
|
|
11050
|
+
const srcPath = path26.join(skill.baseDir, file);
|
|
11051
|
+
const destPath = path26.join(destDir, file);
|
|
11052
|
+
const resolved2 = path26.resolve(destPath);
|
|
10267
11053
|
if (!isInside(resolved2, destDir)) {
|
|
10268
11054
|
throw new FsError({
|
|
10269
11055
|
message: `Path traversal detected in skill file: ${file}`,
|
|
@@ -10272,17 +11058,17 @@ var SkillInstaller = class {
|
|
|
10272
11058
|
context: { reason: "path_traversal", skillName: skill.name }
|
|
10273
11059
|
});
|
|
10274
11060
|
}
|
|
10275
|
-
const
|
|
10276
|
-
if (
|
|
11061
|
+
const stat8 = await fs17.stat(srcPath);
|
|
11062
|
+
if (stat8.size > MAX_SKILL_FILE_SIZE) {
|
|
10277
11063
|
throw new FsError({
|
|
10278
|
-
message: `Skill file "${file}" is too large (${(
|
|
11064
|
+
message: `Skill file "${file}" is too large (${(stat8.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`,
|
|
10279
11065
|
code: ERROR_CODES.FS_WRITE_FAILED,
|
|
10280
11066
|
path: srcPath,
|
|
10281
|
-
context: { skillName: skill.name, fileSize:
|
|
11067
|
+
context: { skillName: skill.name, fileSize: stat8.size, maxSize: MAX_SKILL_FILE_SIZE }
|
|
10282
11068
|
});
|
|
10283
11069
|
}
|
|
10284
|
-
await
|
|
10285
|
-
await
|
|
11070
|
+
await fs17.mkdir(path26.dirname(destPath), { recursive: true });
|
|
11071
|
+
await fs17.copyFile(srcPath, destPath);
|
|
10286
11072
|
copiedFiles.push(file);
|
|
10287
11073
|
}
|
|
10288
11074
|
const entry = {
|
|
@@ -10312,7 +11098,7 @@ var SkillInstaller = class {
|
|
|
10312
11098
|
this.invalidateLoaderCache();
|
|
10313
11099
|
return results;
|
|
10314
11100
|
} finally {
|
|
10315
|
-
await
|
|
11101
|
+
await fs17.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
10316
11102
|
});
|
|
10317
11103
|
}
|
|
10318
11104
|
}
|
|
@@ -10327,7 +11113,7 @@ var SkillInstaller = class {
|
|
|
10327
11113
|
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
10328
11114
|
let entries;
|
|
10329
11115
|
try {
|
|
10330
|
-
entries = await
|
|
11116
|
+
entries = await fs17.readdir(srcDir, { withFileTypes: true });
|
|
10331
11117
|
} catch {
|
|
10332
11118
|
throw new WrongStackError({
|
|
10333
11119
|
message: `Source directory not found or not readable: ${srcDir}`,
|
|
@@ -10339,10 +11125,10 @@ var SkillInstaller = class {
|
|
|
10339
11125
|
const results = [];
|
|
10340
11126
|
for (const e of entries) {
|
|
10341
11127
|
if (!await entryIsDirectory(srcDir, e)) continue;
|
|
10342
|
-
const skillMdPath =
|
|
11128
|
+
const skillMdPath = path26.join(srcDir, e.name, "SKILL.md");
|
|
10343
11129
|
let content;
|
|
10344
11130
|
try {
|
|
10345
|
-
content = await
|
|
11131
|
+
content = await fs17.readFile(skillMdPath, "utf8");
|
|
10346
11132
|
} catch {
|
|
10347
11133
|
continue;
|
|
10348
11134
|
}
|
|
@@ -10352,15 +11138,15 @@ var SkillInstaller = class {
|
|
|
10352
11138
|
if (existing.find((x) => x.scope === scope)) {
|
|
10353
11139
|
await this.removeSkillFiles(fm.name, scope);
|
|
10354
11140
|
}
|
|
10355
|
-
const destDir =
|
|
10356
|
-
await
|
|
10357
|
-
const srcSkillDir =
|
|
11141
|
+
const destDir = path26.join(targetDir, fm.name);
|
|
11142
|
+
await fs17.mkdir(destDir, { recursive: true });
|
|
11143
|
+
const srcSkillDir = path26.join(srcDir, e.name);
|
|
10358
11144
|
const files = await collectFiles(srcSkillDir, srcSkillDir);
|
|
10359
11145
|
const copiedFiles = [];
|
|
10360
11146
|
for (const file of files) {
|
|
10361
|
-
const srcPath =
|
|
10362
|
-
const destPath =
|
|
10363
|
-
const resolved =
|
|
11147
|
+
const srcPath = path26.join(srcSkillDir, file);
|
|
11148
|
+
const destPath = path26.join(destDir, file);
|
|
11149
|
+
const resolved = path26.resolve(destPath);
|
|
10364
11150
|
if (!isInside(resolved, destDir)) {
|
|
10365
11151
|
throw new FsError({
|
|
10366
11152
|
message: `Path traversal detected in skill file: ${file}`,
|
|
@@ -10369,16 +11155,16 @@ var SkillInstaller = class {
|
|
|
10369
11155
|
context: { reason: "path_traversal", skillName: fm.name }
|
|
10370
11156
|
});
|
|
10371
11157
|
}
|
|
10372
|
-
await
|
|
11158
|
+
await fs17.mkdir(path26.dirname(destPath), { recursive: true });
|
|
10373
11159
|
if (opts?.link) {
|
|
10374
11160
|
try {
|
|
10375
|
-
await
|
|
11161
|
+
await fs17.symlink(srcPath, destPath);
|
|
10376
11162
|
} catch (err) {
|
|
10377
|
-
await
|
|
11163
|
+
await fs17.copyFile(srcPath, destPath);
|
|
10378
11164
|
this.opts.log?.(`symlink failed for ${file} (${toErrorMessage(err)}); copied instead`);
|
|
10379
11165
|
}
|
|
10380
11166
|
} else {
|
|
10381
|
-
await
|
|
11167
|
+
await fs17.copyFile(srcPath, destPath);
|
|
10382
11168
|
}
|
|
10383
11169
|
copiedFiles.push(file);
|
|
10384
11170
|
}
|
|
@@ -10532,10 +11318,10 @@ var SkillInstaller = class {
|
|
|
10532
11318
|
*/
|
|
10533
11319
|
async detectSkills(baseDir) {
|
|
10534
11320
|
const results = [];
|
|
10535
|
-
const rootSkillMd =
|
|
11321
|
+
const rootSkillMd = path26.join(baseDir, "SKILL.md");
|
|
10536
11322
|
try {
|
|
10537
|
-
await
|
|
10538
|
-
const content = await
|
|
11323
|
+
await fs17.access(rootSkillMd);
|
|
11324
|
+
const content = await fs17.readFile(rootSkillMd, "utf8");
|
|
10539
11325
|
const fm = parseSkillFrontmatter(content);
|
|
10540
11326
|
if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
|
|
10541
11327
|
results.push({
|
|
@@ -10547,17 +11333,17 @@ var SkillInstaller = class {
|
|
|
10547
11333
|
}
|
|
10548
11334
|
} catch {
|
|
10549
11335
|
}
|
|
10550
|
-
const skillsDir =
|
|
11336
|
+
const skillsDir = path26.join(baseDir, "skills");
|
|
10551
11337
|
try {
|
|
10552
|
-
const entries = await
|
|
11338
|
+
const entries = await fs17.readdir(skillsDir, { withFileTypes: true });
|
|
10553
11339
|
for (const entry of entries) {
|
|
10554
11340
|
if (!entry.isDirectory()) continue;
|
|
10555
|
-
const skillFile =
|
|
11341
|
+
const skillFile = path26.join(skillsDir, entry.name, "SKILL.md");
|
|
10556
11342
|
try {
|
|
10557
|
-
const content = await
|
|
11343
|
+
const content = await fs17.readFile(skillFile, "utf8");
|
|
10558
11344
|
const fm = parseSkillFrontmatter(content);
|
|
10559
11345
|
if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
|
|
10560
|
-
const skillDir =
|
|
11346
|
+
const skillDir = path26.join(skillsDir, entry.name);
|
|
10561
11347
|
const files = await collectFiles(skillDir, skillDir);
|
|
10562
11348
|
results.push({
|
|
10563
11349
|
name: fm.name,
|
|
@@ -10590,8 +11376,8 @@ var SkillInstaller = class {
|
|
|
10590
11376
|
*/
|
|
10591
11377
|
async removeSkillFiles(name, scope) {
|
|
10592
11378
|
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
10593
|
-
const root =
|
|
10594
|
-
const skillDir =
|
|
11379
|
+
const root = path26.resolve(targetDir);
|
|
11380
|
+
const skillDir = path26.resolve(path26.join(targetDir, name));
|
|
10595
11381
|
if (skillDir === root || !isInside(skillDir, root)) {
|
|
10596
11382
|
throw new FsError({
|
|
10597
11383
|
message: `Refusing to delete skill files outside the skills directory: ${name}`,
|
|
@@ -10600,7 +11386,7 @@ var SkillInstaller = class {
|
|
|
10600
11386
|
context: { reason: "path_traversal", skillName: name, scope }
|
|
10601
11387
|
});
|
|
10602
11388
|
}
|
|
10603
|
-
await
|
|
11389
|
+
await fs17.rm(skillDir, { recursive: true, force: true });
|
|
10604
11390
|
}
|
|
10605
11391
|
/**
|
|
10606
11392
|
* Invalidate the skill loader's cache so newly installed skills appear.
|
|
@@ -10635,7 +11421,7 @@ async function entryIsDirectory(dir, entry) {
|
|
|
10635
11421
|
if (entry.isDirectory()) return true;
|
|
10636
11422
|
if (entry.isSymbolicLink()) {
|
|
10637
11423
|
try {
|
|
10638
|
-
return (await
|
|
11424
|
+
return (await fs17.stat(path26.join(dir, entry.name))).isDirectory();
|
|
10639
11425
|
} catch {
|
|
10640
11426
|
return false;
|
|
10641
11427
|
}
|
|
@@ -10644,10 +11430,10 @@ async function entryIsDirectory(dir, entry) {
|
|
|
10644
11430
|
}
|
|
10645
11431
|
async function collectFiles(dir, baseDir) {
|
|
10646
11432
|
const results = [];
|
|
10647
|
-
const entries = await
|
|
11433
|
+
const entries = await fs17.readdir(dir, { withFileTypes: true });
|
|
10648
11434
|
for (const entry of entries) {
|
|
10649
|
-
const fullPath =
|
|
10650
|
-
const relPath =
|
|
11435
|
+
const fullPath = path26.join(dir, entry.name);
|
|
11436
|
+
const relPath = path26.relative(baseDir, fullPath);
|
|
10651
11437
|
if (entry.isDirectory()) {
|
|
10652
11438
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
10653
11439
|
results.push(...await collectFiles(fullPath, baseDir));
|
|
@@ -10726,7 +11512,7 @@ function createSkillsPlugin(opts) {
|
|
|
10726
11512
|
function makeInstaller(skillLoader, projectRoot, registryAdapters) {
|
|
10727
11513
|
const paths = resolveWstackPaths({ projectRoot });
|
|
10728
11514
|
return new SkillInstaller({
|
|
10729
|
-
manifestPath:
|
|
11515
|
+
manifestPath: path27.join(paths.configDir, "installed-skills.json"),
|
|
10730
11516
|
projectSkillsDir: paths.inProjectSkills,
|
|
10731
11517
|
globalSkillsDir: paths.globalSkills,
|
|
10732
11518
|
projectHash: paths.projectHash,
|
|
@@ -11105,7 +11891,7 @@ function resolveImportSourceDir(tool, opts) {
|
|
|
11105
11891
|
const entry = IMPORT_SOURCE_TOOLS.find((t) => t.id === tool);
|
|
11106
11892
|
if (!entry) return void 0;
|
|
11107
11893
|
const base = opts.global ? opts.homeDir ?? os7.homedir() : opts.projectRoot;
|
|
11108
|
-
return
|
|
11894
|
+
return path27.join(base, "." + entry.id, entry.subdir);
|
|
11109
11895
|
}
|
|
11110
11896
|
function buildSkillImportCommand(skillLoader) {
|
|
11111
11897
|
return {
|
|
@@ -11146,7 +11932,7 @@ function buildSkillImportCommand(skillLoader) {
|
|
|
11146
11932
|
};
|
|
11147
11933
|
}
|
|
11148
11934
|
} else if (positional) {
|
|
11149
|
-
srcDir =
|
|
11935
|
+
srcDir = path27.resolve(ctx.projectRoot, positional);
|
|
11150
11936
|
} else {
|
|
11151
11937
|
return {
|
|
11152
11938
|
message: "Usage: /skill-import <src-dir> | --from <tool> | --from-claude [--global] [--link]"
|
|
@@ -11273,14 +12059,14 @@ function truncate(s, max) {
|
|
|
11273
12059
|
}
|
|
11274
12060
|
|
|
11275
12061
|
// src/plugins/sync-plugin.ts
|
|
11276
|
-
import * as
|
|
12062
|
+
import * as path29 from "node:path";
|
|
11277
12063
|
init_error();
|
|
11278
12064
|
|
|
11279
12065
|
// src/storage/cloud-sync.ts
|
|
11280
12066
|
init_atomic_write();
|
|
11281
12067
|
init_errors();
|
|
11282
|
-
import * as
|
|
11283
|
-
import * as
|
|
12068
|
+
import * as fs18 from "node:fs/promises";
|
|
12069
|
+
import * as path28 from "node:path";
|
|
11284
12070
|
import { createHash as createHash9 } from "node:crypto";
|
|
11285
12071
|
var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
|
|
11286
12072
|
var CloudSync = class {
|
|
@@ -11288,8 +12074,8 @@ var CloudSync = class {
|
|
|
11288
12074
|
this.paths = paths;
|
|
11289
12075
|
this.getConfig = getConfig;
|
|
11290
12076
|
this.setConfig = setConfig;
|
|
11291
|
-
this.statePath =
|
|
11292
|
-
this.getSettingsConfigPath = getSettingsConfigPath ?? (() =>
|
|
12077
|
+
this.statePath = path28.join(paths.configDir, "sync-state.json");
|
|
12078
|
+
this.getSettingsConfigPath = getSettingsConfigPath ?? (() => path28.join(this.paths.configDir, "config.json"));
|
|
11293
12079
|
}
|
|
11294
12080
|
paths;
|
|
11295
12081
|
getConfig;
|
|
@@ -11444,7 +12230,7 @@ var CloudSync = class {
|
|
|
11444
12230
|
}
|
|
11445
12231
|
async loadState() {
|
|
11446
12232
|
try {
|
|
11447
|
-
const raw = await
|
|
12233
|
+
const raw = await fs18.readFile(this.statePath, "utf8");
|
|
11448
12234
|
this.state = JSON.parse(raw);
|
|
11449
12235
|
} catch {
|
|
11450
12236
|
this.state = null;
|
|
@@ -11569,18 +12355,18 @@ var CloudSync = class {
|
|
|
11569
12355
|
const localPath = this.categoryToPath(cat);
|
|
11570
12356
|
if (!localPath) continue;
|
|
11571
12357
|
try {
|
|
11572
|
-
const
|
|
11573
|
-
if (
|
|
11574
|
-
if (
|
|
12358
|
+
const stat8 = await fs18.lstat(localPath);
|
|
12359
|
+
if (stat8.isSymbolicLink()) continue;
|
|
12360
|
+
if (stat8.isDirectory()) {
|
|
11575
12361
|
const files = await this.walkDir(localPath, localPath);
|
|
11576
12362
|
for (const file of files) {
|
|
11577
|
-
const content = await
|
|
11578
|
-
const rel =
|
|
12363
|
+
const content = await fs18.readFile(file, "utf8");
|
|
12364
|
+
const rel = path28.relative(localPath, file).replace(/\\/g, "/");
|
|
11579
12365
|
entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
|
|
11580
12366
|
hashes.push(`${cat}/${rel}\0${content}`);
|
|
11581
12367
|
}
|
|
11582
12368
|
} else {
|
|
11583
|
-
const content = await
|
|
12369
|
+
const content = await fs18.readFile(localPath, "utf8");
|
|
11584
12370
|
entries.push({ path: `data/${cat}`, content, mode: "100644" });
|
|
11585
12371
|
hashes.push(`${cat}\0${content}`);
|
|
11586
12372
|
}
|
|
@@ -11596,17 +12382,17 @@ var CloudSync = class {
|
|
|
11596
12382
|
const localPath = this.categoryToPath(cat);
|
|
11597
12383
|
if (!localPath) continue;
|
|
11598
12384
|
try {
|
|
11599
|
-
const
|
|
11600
|
-
if (
|
|
11601
|
-
if (
|
|
12385
|
+
const stat8 = await fs18.lstat(localPath);
|
|
12386
|
+
if (stat8.isSymbolicLink()) continue;
|
|
12387
|
+
if (stat8.isDirectory()) {
|
|
11602
12388
|
const files = await this.walkDir(localPath, localPath);
|
|
11603
12389
|
for (const file of files) {
|
|
11604
|
-
const content = await
|
|
11605
|
-
const rel =
|
|
12390
|
+
const content = await fs18.readFile(file, "utf8");
|
|
12391
|
+
const rel = path28.relative(localPath, file).replace(/\\/g, "/");
|
|
11606
12392
|
hashes.push(`${cat}/${rel}\0${content}`);
|
|
11607
12393
|
}
|
|
11608
12394
|
} else {
|
|
11609
|
-
const content = await
|
|
12395
|
+
const content = await fs18.readFile(localPath, "utf8");
|
|
11610
12396
|
hashes.push(`${cat}\0${content}`);
|
|
11611
12397
|
}
|
|
11612
12398
|
} catch {
|
|
@@ -11633,10 +12419,10 @@ var CloudSync = class {
|
|
|
11633
12419
|
}
|
|
11634
12420
|
async walkDir(dir, base) {
|
|
11635
12421
|
const results = [];
|
|
11636
|
-
const entries = await
|
|
12422
|
+
const entries = await fs18.readdir(dir, { withFileTypes: true });
|
|
11637
12423
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
11638
12424
|
for (const entry of entries) {
|
|
11639
|
-
const full =
|
|
12425
|
+
const full = path28.join(dir, entry.name);
|
|
11640
12426
|
if (entry.isSymbolicLink()) continue;
|
|
11641
12427
|
if (entry.isDirectory()) {
|
|
11642
12428
|
results.push(...await this.walkDir(full, base));
|
|
@@ -11649,29 +12435,29 @@ var CloudSync = class {
|
|
|
11649
12435
|
};
|
|
11650
12436
|
async function preparePulledDestination(cat, localPath, destPath, remotePath) {
|
|
11651
12437
|
const directoryBacked = cat === "skills" || cat === "prompts";
|
|
11652
|
-
const rootPath = directoryBacked ? localPath :
|
|
12438
|
+
const rootPath = directoryBacked ? localPath : path28.dirname(localPath);
|
|
11653
12439
|
const rootStat = await lstatIfExists(rootPath);
|
|
11654
12440
|
if (rootStat?.isSymbolicLink()) {
|
|
11655
12441
|
throw unsafePulledSymlinkError(remotePath, rootPath);
|
|
11656
12442
|
}
|
|
11657
12443
|
if (directoryBacked) {
|
|
11658
|
-
const relativeParent =
|
|
12444
|
+
const relativeParent = path28.relative(rootPath, path28.dirname(destPath));
|
|
11659
12445
|
let cursor = rootPath;
|
|
11660
12446
|
if (relativeParent) {
|
|
11661
|
-
for (const segment of relativeParent.split(
|
|
11662
|
-
cursor =
|
|
11663
|
-
const
|
|
11664
|
-
if (
|
|
12447
|
+
for (const segment of relativeParent.split(path28.sep)) {
|
|
12448
|
+
cursor = path28.join(cursor, segment);
|
|
12449
|
+
const stat8 = await lstatIfExists(cursor);
|
|
12450
|
+
if (stat8?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, cursor);
|
|
11665
12451
|
}
|
|
11666
12452
|
}
|
|
11667
12453
|
}
|
|
11668
12454
|
const destStat = await lstatIfExists(destPath);
|
|
11669
12455
|
if (destStat?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, destPath);
|
|
11670
|
-
await
|
|
12456
|
+
await fs18.mkdir(path28.dirname(destPath), { recursive: true });
|
|
11671
12457
|
}
|
|
11672
12458
|
async function lstatIfExists(filePath) {
|
|
11673
12459
|
try {
|
|
11674
|
-
return await
|
|
12460
|
+
return await fs18.lstat(filePath);
|
|
11675
12461
|
} catch (err) {
|
|
11676
12462
|
if (err.code === "ENOENT") return null;
|
|
11677
12463
|
throw err;
|
|
@@ -11697,9 +12483,9 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
|
11697
12483
|
return localPath;
|
|
11698
12484
|
}
|
|
11699
12485
|
if (!rel) return localPath;
|
|
11700
|
-
const normalizedRel =
|
|
11701
|
-
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${
|
|
11702
|
-
if (
|
|
12486
|
+
const normalizedRel = path28.normalize(rel);
|
|
12487
|
+
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path28.sep}`);
|
|
12488
|
+
if (path28.isAbsolute(normalizedRel) || traversesUp) {
|
|
11703
12489
|
throw new FsError({
|
|
11704
12490
|
message: `Refusing CloudSync path traversal: ${remotePath}`,
|
|
11705
12491
|
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
@@ -11707,10 +12493,10 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
|
11707
12493
|
context: { reason: "path_traversal", normalizedRel }
|
|
11708
12494
|
});
|
|
11709
12495
|
}
|
|
11710
|
-
const dest =
|
|
11711
|
-
const root =
|
|
11712
|
-
const relative5 =
|
|
11713
|
-
if (relative5.startsWith("..") ||
|
|
12496
|
+
const dest = path28.resolve(localPath, normalizedRel);
|
|
12497
|
+
const root = path28.resolve(localPath);
|
|
12498
|
+
const relative5 = path28.relative(root, dest);
|
|
12499
|
+
if (relative5.startsWith("..") || path28.isAbsolute(relative5)) {
|
|
11714
12500
|
throw new FsError({
|
|
11715
12501
|
message: `Refusing CloudSync path outside category root: ${remotePath}`,
|
|
11716
12502
|
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
@@ -11753,7 +12539,7 @@ function createSyncPlugin(opts) {
|
|
|
11753
12539
|
api.log.warn("[sync] paths, configStore, or configDir not available \u2014 /sync disabled");
|
|
11754
12540
|
return;
|
|
11755
12541
|
}
|
|
11756
|
-
const syncConfigPath = paths.syncConfig ??
|
|
12542
|
+
const syncConfigPath = paths.syncConfig ?? path29.join(paths.configDir, "sync.json");
|
|
11757
12543
|
cloud = new CloudSync(
|
|
11758
12544
|
paths,
|
|
11759
12545
|
() => {
|
|
@@ -11764,7 +12550,7 @@ function createSyncPlugin(opts) {
|
|
|
11764
12550
|
await persistSyncConfig(syncConfigPath, cfg, vault);
|
|
11765
12551
|
configStore?.update({ sync: cfg });
|
|
11766
12552
|
},
|
|
11767
|
-
() =>
|
|
12553
|
+
() => path29.join(paths.configDir, "config.json")
|
|
11768
12554
|
);
|
|
11769
12555
|
void cloud.loadState();
|
|
11770
12556
|
api.slashCommands.register(buildSyncCommand(cloud, configStore, vault, syncConfigPath));
|
|
@@ -11954,6 +12740,852 @@ Warning: sync completed, but metadata persistence failed: ${toErrorMessage(err)}
|
|
|
11954
12740
|
}
|
|
11955
12741
|
};
|
|
11956
12742
|
}
|
|
12743
|
+
|
|
12744
|
+
// src/plugins/cloud-config-sync-plugin.ts
|
|
12745
|
+
import * as path31 from "node:path";
|
|
12746
|
+
|
|
12747
|
+
// src/security/config-secrets.ts
|
|
12748
|
+
function decryptConfigSecrets(cfg, vault, opts) {
|
|
12749
|
+
const warn = opts?.warn ?? ((msg) => console.warn(msg));
|
|
12750
|
+
return walk2(cfg, vault, (v, key) => {
|
|
12751
|
+
try {
|
|
12752
|
+
return vault.decrypt(v);
|
|
12753
|
+
} catch (err) {
|
|
12754
|
+
warn(
|
|
12755
|
+
`[secret-vault] Failed to decrypt "${key}": ${err instanceof Error ? err.message : err}`
|
|
12756
|
+
);
|
|
12757
|
+
return "";
|
|
12758
|
+
}
|
|
12759
|
+
});
|
|
12760
|
+
}
|
|
12761
|
+
function encryptConfigSecrets(cfg, vault, _opts) {
|
|
12762
|
+
return walk2(cfg, vault, (v) => vault.encrypt(v));
|
|
12763
|
+
}
|
|
12764
|
+
function walk2(node, vault, transform) {
|
|
12765
|
+
if (node === null || node === void 0) return node;
|
|
12766
|
+
if (typeof node !== "object") return node;
|
|
12767
|
+
if (Array.isArray(node)) {
|
|
12768
|
+
return node.map((item) => walk2(item, vault, transform));
|
|
12769
|
+
}
|
|
12770
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
12771
|
+
for (const [k, v] of Object.entries(node)) {
|
|
12772
|
+
if (typeof v === "string" && isSecretField(k)) {
|
|
12773
|
+
out[k] = transform(v, k);
|
|
12774
|
+
} else if (typeof v === "object" && v !== null) {
|
|
12775
|
+
out[k] = walk2(v, vault, transform);
|
|
12776
|
+
} else {
|
|
12777
|
+
out[k] = v;
|
|
12778
|
+
}
|
|
12779
|
+
}
|
|
12780
|
+
return out;
|
|
12781
|
+
}
|
|
12782
|
+
var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key|token\b)/i;
|
|
12783
|
+
var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
|
|
12784
|
+
function isSecretField(name) {
|
|
12785
|
+
const lc = name.toLowerCase();
|
|
12786
|
+
if (NON_SECRET_OVERRIDES.has(lc)) return false;
|
|
12787
|
+
return SECRET_KEY_PATTERN.test(lc);
|
|
12788
|
+
}
|
|
12789
|
+
|
|
12790
|
+
// src/storage/cloud-config-sync.ts
|
|
12791
|
+
init_atomic_write();
|
|
12792
|
+
import { createHash as createHash10, randomUUID as nodeRandomUUID } from "node:crypto";
|
|
12793
|
+
import * as fs19 from "node:fs/promises";
|
|
12794
|
+
import * as path30 from "node:path";
|
|
12795
|
+
|
|
12796
|
+
// src/storage/cloud-config-sync/sanitize.ts
|
|
12797
|
+
var SAGE_TREE = {
|
|
12798
|
+
enabled: true,
|
|
12799
|
+
storage: { projectLocal: true, directory: true },
|
|
12800
|
+
inject: {
|
|
12801
|
+
turnContext: true,
|
|
12802
|
+
toolResults: true,
|
|
12803
|
+
taskAware: true,
|
|
12804
|
+
maxHintsPerTool: true,
|
|
12805
|
+
maxCharsPerTool: true,
|
|
12806
|
+
minScore: true,
|
|
12807
|
+
repeatCooldownMs: true,
|
|
12808
|
+
maxTurnMemories: true,
|
|
12809
|
+
maxCharsPerTurn: true,
|
|
12810
|
+
minImportance: true,
|
|
12811
|
+
relationFloor: true
|
|
12812
|
+
},
|
|
12813
|
+
hygiene: {
|
|
12814
|
+
autoAfterSession: true,
|
|
12815
|
+
autoOnFileChange: true,
|
|
12816
|
+
retentionDays: true,
|
|
12817
|
+
archiveLowConfidenceAfterDays: true
|
|
12818
|
+
},
|
|
12819
|
+
embeddings: { enabled: true }
|
|
12820
|
+
};
|
|
12821
|
+
var CORE_RUNTIME_TREE = {
|
|
12822
|
+
maxConcurrent: true,
|
|
12823
|
+
context: {
|
|
12824
|
+
mode: true,
|
|
12825
|
+
autoCompact: true,
|
|
12826
|
+
strategy: true,
|
|
12827
|
+
warnThreshold: true,
|
|
12828
|
+
softThreshold: true,
|
|
12829
|
+
hardThreshold: true,
|
|
12830
|
+
preserveK: true,
|
|
12831
|
+
eliseThreshold: true,
|
|
12832
|
+
targetLoad: true
|
|
12833
|
+
},
|
|
12834
|
+
tools: {
|
|
12835
|
+
defaultExecutionStrategy: true,
|
|
12836
|
+
maxIterations: true,
|
|
12837
|
+
iterationTimeoutMs: true,
|
|
12838
|
+
sessionTimeoutMs: true,
|
|
12839
|
+
perIterationOutputCapBytes: true,
|
|
12840
|
+
autoExtendLimit: true,
|
|
12841
|
+
restrictToProjectRoot: true,
|
|
12842
|
+
descriptionMode: true,
|
|
12843
|
+
disabledTools: true,
|
|
12844
|
+
maxToolTimeoutMs: true,
|
|
12845
|
+
loopDetection: {
|
|
12846
|
+
mode: true,
|
|
12847
|
+
steerThreshold: true,
|
|
12848
|
+
cutThreshold: true,
|
|
12849
|
+
windowSize: true,
|
|
12850
|
+
callRepeatThreshold: true
|
|
12851
|
+
}
|
|
12852
|
+
},
|
|
12853
|
+
log: { level: true, format: true },
|
|
12854
|
+
features: {
|
|
12855
|
+
mcp: true,
|
|
12856
|
+
plugins: true,
|
|
12857
|
+
memory: true,
|
|
12858
|
+
modelsRegistry: true,
|
|
12859
|
+
skills: true,
|
|
12860
|
+
prompts: true,
|
|
12861
|
+
tokenSavingMode: true,
|
|
12862
|
+
allowOutsideProjectRoot: true,
|
|
12863
|
+
developerMode: true
|
|
12864
|
+
},
|
|
12865
|
+
session: { auditLevel: true, sampling: { toolProgress: { sampleRate: true } } },
|
|
12866
|
+
indexing: { onSessionStart: true, onEdit: true, watchExternal: true, debounceMs: true },
|
|
12867
|
+
circuitBreaker: { enabled: true, autoKillResetMs: true },
|
|
12868
|
+
modelRuntime: {
|
|
12869
|
+
reasoning: { mode: true, effort: true, preserve: true },
|
|
12870
|
+
cache: { ttl: true }
|
|
12871
|
+
},
|
|
12872
|
+
systemPrompt: { variant: true },
|
|
12873
|
+
skills: { readClaudeSkills: true },
|
|
12874
|
+
Sage: SAGE_TREE,
|
|
12875
|
+
fallbackAuto: true,
|
|
12876
|
+
yolo: true,
|
|
12877
|
+
nextPrediction: true,
|
|
12878
|
+
hints: true,
|
|
12879
|
+
debugStream: true
|
|
12880
|
+
};
|
|
12881
|
+
var UI_PREFERENCES_TREE = {
|
|
12882
|
+
autonomy: {
|
|
12883
|
+
defaultMode: true,
|
|
12884
|
+
autoProceedDelayMs: true,
|
|
12885
|
+
autoProceedMaxIterations: true,
|
|
12886
|
+
autonomyNextPrompt: true,
|
|
12887
|
+
terminalTitleAnimation: true,
|
|
12888
|
+
yolo: true,
|
|
12889
|
+
streamFleet: true,
|
|
12890
|
+
chime: true,
|
|
12891
|
+
confirmExit: true,
|
|
12892
|
+
mouseMode: true,
|
|
12893
|
+
enhance: true,
|
|
12894
|
+
enhanceDelayMs: true,
|
|
12895
|
+
enhanceCountdownMs: true,
|
|
12896
|
+
enhanceLanguage: true,
|
|
12897
|
+
statuslineMode: true,
|
|
12898
|
+
thinkingWord: true,
|
|
12899
|
+
midRunSendPicker: true,
|
|
12900
|
+
animationStyle: true,
|
|
12901
|
+
fleetChatVerbosity: true,
|
|
12902
|
+
refinerFallbackProfile: true,
|
|
12903
|
+
refinerProvider: true,
|
|
12904
|
+
refinerModel: true,
|
|
12905
|
+
showModelReasoning: true,
|
|
12906
|
+
shellBangWarningDontShowAgain: true,
|
|
12907
|
+
showAgentSwarmPanel: true,
|
|
12908
|
+
showSageMemoryInject: true,
|
|
12909
|
+
readAdvancedMode: true,
|
|
12910
|
+
panelPositions: true
|
|
12911
|
+
},
|
|
12912
|
+
launch: { mode: true, autonomy: true },
|
|
12913
|
+
uiLocale: true
|
|
12914
|
+
};
|
|
12915
|
+
var MODELS_ROUTING_TREE = {
|
|
12916
|
+
provider: true,
|
|
12917
|
+
model: true,
|
|
12918
|
+
favoriteModels: true,
|
|
12919
|
+
favoriteModelsOnly: true,
|
|
12920
|
+
fallbackModels: true,
|
|
12921
|
+
fallbackProfiles: true,
|
|
12922
|
+
modelMatrix: { "*": { fallbackProfile: true } },
|
|
12923
|
+
brain: {
|
|
12924
|
+
mode: true,
|
|
12925
|
+
maxAutoRisk: true,
|
|
12926
|
+
models: true,
|
|
12927
|
+
strategy: true,
|
|
12928
|
+
decisionTimeoutMs: true,
|
|
12929
|
+
humanTimeoutMs: true,
|
|
12930
|
+
council: {
|
|
12931
|
+
enabled: true,
|
|
12932
|
+
minRisk: true,
|
|
12933
|
+
voters: true
|
|
12934
|
+
},
|
|
12935
|
+
ledger: { enabled: true, autoDenyAfterFailures: true },
|
|
12936
|
+
trace: { enabled: true }
|
|
12937
|
+
}
|
|
12938
|
+
};
|
|
12939
|
+
var PROVIDERS_CATALOG_TREE = {
|
|
12940
|
+
providers: {
|
|
12941
|
+
"*": {
|
|
12942
|
+
type: true,
|
|
12943
|
+
family: true,
|
|
12944
|
+
baseUrl: true,
|
|
12945
|
+
envVars: true,
|
|
12946
|
+
models: true,
|
|
12947
|
+
activeKey: true
|
|
12948
|
+
}
|
|
12949
|
+
}
|
|
12950
|
+
};
|
|
12951
|
+
var MCP_SERVERS_TREE = {
|
|
12952
|
+
mcpServers: {
|
|
12953
|
+
"*": {
|
|
12954
|
+
name: true,
|
|
12955
|
+
transport: true,
|
|
12956
|
+
command: true,
|
|
12957
|
+
args: true,
|
|
12958
|
+
url: true,
|
|
12959
|
+
description: true,
|
|
12960
|
+
permission: true,
|
|
12961
|
+
enabled: true,
|
|
12962
|
+
lazy: true,
|
|
12963
|
+
envVars: true
|
|
12964
|
+
}
|
|
12965
|
+
}
|
|
12966
|
+
};
|
|
12967
|
+
var EXTENSIONS_PLUGINS_TREE = {
|
|
12968
|
+
plugins: true,
|
|
12969
|
+
extensions: true
|
|
12970
|
+
};
|
|
12971
|
+
var CLOUD_SYNC_CONTRACT = {
|
|
12972
|
+
"core.runtime": CORE_RUNTIME_TREE,
|
|
12973
|
+
"ui.preferences": UI_PREFERENCES_TREE,
|
|
12974
|
+
"models.routing": MODELS_ROUTING_TREE,
|
|
12975
|
+
"providers.catalog": PROVIDERS_CATALOG_TREE,
|
|
12976
|
+
"mcp.servers": MCP_SERVERS_TREE,
|
|
12977
|
+
"extensions.plugins": EXTENSIONS_PLUGINS_TREE
|
|
12978
|
+
};
|
|
12979
|
+
var CLOUD_SYNC_NAMESPACES = Object.keys(CLOUD_SYNC_CONTRACT);
|
|
12980
|
+
var NAMESPACE_SCHEMA_VERSIONS = {
|
|
12981
|
+
"core.runtime": 1,
|
|
12982
|
+
"ui.preferences": 1,
|
|
12983
|
+
"models.routing": 1,
|
|
12984
|
+
"providers.catalog": 1,
|
|
12985
|
+
"mcp.servers": 2,
|
|
12986
|
+
"extensions.plugins": 1
|
|
12987
|
+
};
|
|
12988
|
+
var ENCRYPTED_VALUE = /enc:v\d+:/;
|
|
12989
|
+
var SECRET_KEY = /^(api[-_]?key|apikeys|.*token|secret.*|.*secret|password|passwd|credential.*|private[-_]?key|authorization)$/i;
|
|
12990
|
+
function isPlainObject2(value) {
|
|
12991
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12992
|
+
}
|
|
12993
|
+
function stripSecretMaterial(value) {
|
|
12994
|
+
if (typeof value === "string") {
|
|
12995
|
+
return ENCRYPTED_VALUE.test(value) ? void 0 : value;
|
|
12996
|
+
}
|
|
12997
|
+
if (Array.isArray(value)) {
|
|
12998
|
+
const cleaned = value.map((entry) => stripSecretMaterial(entry));
|
|
12999
|
+
return cleaned.filter((entry) => entry !== void 0);
|
|
13000
|
+
}
|
|
13001
|
+
if (isPlainObject2(value)) {
|
|
13002
|
+
const out = {};
|
|
13003
|
+
for (const [key, child] of Object.entries(value)) {
|
|
13004
|
+
if (SECRET_KEY.test(key)) continue;
|
|
13005
|
+
const cleaned = stripSecretMaterial(child);
|
|
13006
|
+
if (cleaned !== void 0) out[key] = cleaned;
|
|
13007
|
+
}
|
|
13008
|
+
return out;
|
|
13009
|
+
}
|
|
13010
|
+
return value;
|
|
13011
|
+
}
|
|
13012
|
+
function project(value, tree) {
|
|
13013
|
+
if (tree === true) return value;
|
|
13014
|
+
if (!isPlainObject2(value)) return void 0;
|
|
13015
|
+
const wildcard = tree["*"];
|
|
13016
|
+
const out = {};
|
|
13017
|
+
for (const [key, child] of Object.entries(value)) {
|
|
13018
|
+
const node = Object.hasOwn(tree, key) && key !== "*" ? tree[key] : wildcard;
|
|
13019
|
+
if (node === void 0) continue;
|
|
13020
|
+
const projected = project(child, node);
|
|
13021
|
+
if (projected !== void 0) out[key] = projected;
|
|
13022
|
+
}
|
|
13023
|
+
return out;
|
|
13024
|
+
}
|
|
13025
|
+
function buildNamespacePayloads(config) {
|
|
13026
|
+
const payloads = {};
|
|
13027
|
+
for (const [namespace, tree] of Object.entries(CLOUD_SYNC_CONTRACT)) {
|
|
13028
|
+
if (tree === true) continue;
|
|
13029
|
+
const payload = {};
|
|
13030
|
+
for (const [key, node] of Object.entries(tree)) {
|
|
13031
|
+
if (!(key in config) || config[key] === void 0) continue;
|
|
13032
|
+
const projected = project(config[key], node);
|
|
13033
|
+
if (projected === void 0) continue;
|
|
13034
|
+
const cleaned = stripSecretMaterial(projected);
|
|
13035
|
+
if (cleaned === void 0) continue;
|
|
13036
|
+
if (isPlainObject2(cleaned) && Object.keys(cleaned).length === 0 && node !== true) continue;
|
|
13037
|
+
payload[key] = cleaned;
|
|
13038
|
+
}
|
|
13039
|
+
if (Object.keys(payload).length > 0) payloads[namespace] = payload;
|
|
13040
|
+
}
|
|
13041
|
+
return payloads;
|
|
13042
|
+
}
|
|
13043
|
+
function reinjectLocalSecrets(local, incoming) {
|
|
13044
|
+
if (!isPlainObject2(local) || !isPlainObject2(incoming)) return incoming;
|
|
13045
|
+
const out = { ...incoming };
|
|
13046
|
+
for (const [key, value] of Object.entries(local)) {
|
|
13047
|
+
const secretish = SECRET_KEY.test(key) || typeof value === "string" && ENCRYPTED_VALUE.test(value);
|
|
13048
|
+
if (secretish && !(key in incoming)) {
|
|
13049
|
+
out[key] = value;
|
|
13050
|
+
continue;
|
|
13051
|
+
}
|
|
13052
|
+
if (isPlainObject2(value) && isPlainObject2(incoming[key])) {
|
|
13053
|
+
out[key] = reinjectLocalSecrets(value, incoming[key]);
|
|
13054
|
+
}
|
|
13055
|
+
}
|
|
13056
|
+
return out;
|
|
13057
|
+
}
|
|
13058
|
+
function mergeAtContract(local, incoming, tree) {
|
|
13059
|
+
if (tree === true) return reinjectLocalSecrets(local, incoming);
|
|
13060
|
+
if (!isPlainObject2(incoming)) return local;
|
|
13061
|
+
const base = isPlainObject2(local) ? { ...local } : {};
|
|
13062
|
+
const wildcard = tree["*"];
|
|
13063
|
+
for (const [key, child] of Object.entries(incoming)) {
|
|
13064
|
+
const node = Object.hasOwn(tree, key) && key !== "*" ? tree[key] : wildcard;
|
|
13065
|
+
if (node === void 0) continue;
|
|
13066
|
+
base[key] = mergeAtContract(base[key], child, node);
|
|
13067
|
+
}
|
|
13068
|
+
return base;
|
|
13069
|
+
}
|
|
13070
|
+
function applyNamespacePayload(config, namespace, payload) {
|
|
13071
|
+
const tree = CLOUD_SYNC_CONTRACT[namespace];
|
|
13072
|
+
if (!tree || tree === true) return config;
|
|
13073
|
+
const next = { ...config };
|
|
13074
|
+
for (const [key, incoming] of Object.entries(payload)) {
|
|
13075
|
+
const node = Object.hasOwn(tree, key) ? tree[key] : void 0;
|
|
13076
|
+
if (node === void 0) continue;
|
|
13077
|
+
next[key] = mergeAtContract(next[key], incoming, node);
|
|
13078
|
+
}
|
|
13079
|
+
return next;
|
|
13080
|
+
}
|
|
13081
|
+
|
|
13082
|
+
// src/storage/cloud-config-sync.ts
|
|
13083
|
+
var EMPTY_STATE = {
|
|
13084
|
+
version: 1,
|
|
13085
|
+
cursor: 0,
|
|
13086
|
+
namespaceRevisions: {},
|
|
13087
|
+
syncedHashes: {},
|
|
13088
|
+
syncedPayloads: {},
|
|
13089
|
+
pendingMutations: {},
|
|
13090
|
+
rejectedHashes: {}
|
|
13091
|
+
};
|
|
13092
|
+
function stableStringify3(value) {
|
|
13093
|
+
if (Array.isArray(value)) {
|
|
13094
|
+
return `[${value.map((entry) => stableStringify3(entry)).join(",")}]`;
|
|
13095
|
+
}
|
|
13096
|
+
if (typeof value === "object" && value !== null) {
|
|
13097
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify3(v)}`);
|
|
13098
|
+
return `{${entries.join(",")}}`;
|
|
13099
|
+
}
|
|
13100
|
+
return JSON.stringify(value) ?? "null";
|
|
13101
|
+
}
|
|
13102
|
+
function hashPayload(payload) {
|
|
13103
|
+
return createHash10("sha256").update(stableStringify3(payload)).digest("hex");
|
|
13104
|
+
}
|
|
13105
|
+
function deepEquals(a, b) {
|
|
13106
|
+
return stableStringify3(a) === stableStringify3(b);
|
|
13107
|
+
}
|
|
13108
|
+
var CloudConfigSync = class {
|
|
13109
|
+
constructor(deps) {
|
|
13110
|
+
this.deps = deps;
|
|
13111
|
+
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
13112
|
+
this.uuid = deps.randomUUID ?? (() => nodeRandomUUID());
|
|
13113
|
+
this.timeoutMs = deps.requestTimeoutMs ?? 15e3;
|
|
13114
|
+
}
|
|
13115
|
+
deps;
|
|
13116
|
+
state = null;
|
|
13117
|
+
fetchImpl;
|
|
13118
|
+
uuid;
|
|
13119
|
+
timeoutMs;
|
|
13120
|
+
/** One full pull-then-push-then-heartbeat pass. Never throws; failures are summarized. */
|
|
13121
|
+
async syncOnce() {
|
|
13122
|
+
const settings = this.deps.getSettings();
|
|
13123
|
+
if (!settings?.enabled || !settings.url || !settings.token) {
|
|
13124
|
+
return summary({ skipped: true, message: "Cloud sync is not configured." });
|
|
13125
|
+
}
|
|
13126
|
+
const state = await this.loadState();
|
|
13127
|
+
const pulled = [];
|
|
13128
|
+
const pushed = [];
|
|
13129
|
+
const conflicted = [];
|
|
13130
|
+
const rejected = [];
|
|
13131
|
+
try {
|
|
13132
|
+
await this.pullChanges(settings, state, pulled);
|
|
13133
|
+
await this.pushDirty(settings, state, { pushed, conflicted, rejected });
|
|
13134
|
+
await this.heartbeat(settings, state);
|
|
13135
|
+
state.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13136
|
+
await this.saveState(state);
|
|
13137
|
+
return summary({
|
|
13138
|
+
ok: true,
|
|
13139
|
+
pulled,
|
|
13140
|
+
pushed,
|
|
13141
|
+
conflicted,
|
|
13142
|
+
rejected,
|
|
13143
|
+
message: pulled.length + pushed.length === 0 ? "Already in sync." : `Pulled ${pulled.length}, pushed ${pushed.length} namespace(s).`
|
|
13144
|
+
});
|
|
13145
|
+
} catch (error) {
|
|
13146
|
+
await this.saveState(state).catch(() => {
|
|
13147
|
+
});
|
|
13148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13149
|
+
this.deps.logger?.warn(`cloud-config-sync: pass failed: ${message}`);
|
|
13150
|
+
return summary({ pulled, pushed, conflicted, rejected, message });
|
|
13151
|
+
}
|
|
13152
|
+
}
|
|
13153
|
+
/** Human-readable status for the slash command. */
|
|
13154
|
+
async statusText() {
|
|
13155
|
+
const settings = this.deps.getSettings();
|
|
13156
|
+
if (!settings?.enabled) {
|
|
13157
|
+
return "Cloud config sync: disabled. Run `/cloudsync on` after setting url + token.";
|
|
13158
|
+
}
|
|
13159
|
+
const state = await this.loadState();
|
|
13160
|
+
const dirty = await this.dirtyNamespaces(state);
|
|
13161
|
+
const rejectedList = Object.keys(state.rejectedHashes);
|
|
13162
|
+
const lines = [
|
|
13163
|
+
"Cloud config sync: enabled",
|
|
13164
|
+
` url: ${settings.url}`,
|
|
13165
|
+
` cursor: ${state.cursor}`,
|
|
13166
|
+
` last sync: ${state.lastSyncedAt ?? "never"}`,
|
|
13167
|
+
` dirty: ${dirty.length > 0 ? dirty.join(", ") : "none"}`
|
|
13168
|
+
];
|
|
13169
|
+
if (rejectedList.length > 0) {
|
|
13170
|
+
lines.push(` rejected: ${rejectedList.join(", ")} (kept local; edit to retry)`);
|
|
13171
|
+
}
|
|
13172
|
+
return lines.join("\n");
|
|
13173
|
+
}
|
|
13174
|
+
// ── Pull ────────────────────────────────────────────────────────────────
|
|
13175
|
+
async pullChanges(settings, state, pulled) {
|
|
13176
|
+
const delta = (await this.request(settings, "GET", `/api/sync/v1/config?since=${state.cursor}`)).json;
|
|
13177
|
+
const toRead = delta.full ? CLOUD_SYNC_NAMESPACES.map((namespace) => ({ namespace })) : delta.changed.filter(
|
|
13178
|
+
(entry) => CLOUD_SYNC_NAMESPACES.includes(entry.namespace) && entry.revision > (state.namespaceRevisions[entry.namespace] ?? 0)
|
|
13179
|
+
);
|
|
13180
|
+
const dirtyBefore = new Set(await this.dirtyNamespaces(state));
|
|
13181
|
+
for (const entry of toRead) {
|
|
13182
|
+
const snapshot = (await this.request(
|
|
13183
|
+
settings,
|
|
13184
|
+
"GET",
|
|
13185
|
+
`/api/sync/v1/config/${encodeURIComponent(entry.namespace)}`
|
|
13186
|
+
)).json;
|
|
13187
|
+
if ((state.namespaceRevisions[entry.namespace] ?? 0) >= snapshot.revision) continue;
|
|
13188
|
+
if (snapshot.revision === 0) {
|
|
13189
|
+
state.namespaceRevisions[entry.namespace] = 0;
|
|
13190
|
+
continue;
|
|
13191
|
+
}
|
|
13192
|
+
if (dirtyBefore.has(entry.namespace)) {
|
|
13193
|
+
const local = await this.deps.readLocalConfig();
|
|
13194
|
+
const localPayload = buildNamespacePayloads(local)[entry.namespace] ?? {};
|
|
13195
|
+
const base = state.syncedPayloads[entry.namespace] ?? {};
|
|
13196
|
+
const merged = { ...snapshot.payload };
|
|
13197
|
+
for (const [key, localValue] of Object.entries(localPayload)) {
|
|
13198
|
+
if (!deepEquals(localValue, base[key])) merged[key] = localValue;
|
|
13199
|
+
}
|
|
13200
|
+
await this.deps.writeLocalConfig(
|
|
13201
|
+
(config) => applyNamespacePayload(config, entry.namespace, merged)
|
|
13202
|
+
);
|
|
13203
|
+
state.namespaceRevisions[entry.namespace] = snapshot.revision;
|
|
13204
|
+
state.syncedPayloads[entry.namespace] = snapshot.payload;
|
|
13205
|
+
} else {
|
|
13206
|
+
await this.applySnapshot(entry.namespace, snapshot, state);
|
|
13207
|
+
}
|
|
13208
|
+
pulled.push(entry.namespace);
|
|
13209
|
+
}
|
|
13210
|
+
state.cursor = Math.max(state.cursor, delta.cursor);
|
|
13211
|
+
}
|
|
13212
|
+
async applySnapshot(namespace, snapshot, state) {
|
|
13213
|
+
await this.deps.writeLocalConfig(
|
|
13214
|
+
(config) => applyNamespacePayload(config, namespace, snapshot.payload)
|
|
13215
|
+
);
|
|
13216
|
+
state.namespaceRevisions[namespace] = snapshot.revision;
|
|
13217
|
+
state.syncedPayloads[namespace] = snapshot.payload;
|
|
13218
|
+
const local = await this.deps.readLocalConfig();
|
|
13219
|
+
const projected = buildNamespacePayloads(local)[namespace] ?? {};
|
|
13220
|
+
state.syncedHashes[namespace] = hashPayload(projected);
|
|
13221
|
+
delete state.pendingMutations[namespace];
|
|
13222
|
+
delete state.rejectedHashes[namespace];
|
|
13223
|
+
}
|
|
13224
|
+
// ── Push ────────────────────────────────────────────────────────────────
|
|
13225
|
+
async dirtyNamespaces(state) {
|
|
13226
|
+
const local = await this.deps.readLocalConfig();
|
|
13227
|
+
const payloads = buildNamespacePayloads(local);
|
|
13228
|
+
return Object.entries(payloads).filter(([namespace, payload]) => hashPayload(payload) !== state.syncedHashes[namespace]).map(([namespace]) => namespace);
|
|
13229
|
+
}
|
|
13230
|
+
async pushDirty(settings, state, out) {
|
|
13231
|
+
const local = await this.deps.readLocalConfig();
|
|
13232
|
+
const payloads = buildNamespacePayloads(local);
|
|
13233
|
+
for (const [namespace, payload] of Object.entries(payloads)) {
|
|
13234
|
+
const hash = hashPayload(payload);
|
|
13235
|
+
if (hash === state.syncedHashes[namespace]) continue;
|
|
13236
|
+
if (state.rejectedHashes[namespace] === hash) continue;
|
|
13237
|
+
const pending = state.pendingMutations[namespace];
|
|
13238
|
+
const mutationId = pending?.hash === hash ? pending.mutationId : this.uuid();
|
|
13239
|
+
state.pendingMutations[namespace] = { mutationId, hash };
|
|
13240
|
+
await this.saveState(state);
|
|
13241
|
+
const result = await this.putNamespace(settings, state, namespace, payload, mutationId);
|
|
13242
|
+
if (result.kind === "applied") {
|
|
13243
|
+
out.pushed.push(namespace);
|
|
13244
|
+
} else if (result.kind === "conflict") {
|
|
13245
|
+
out.conflicted.push(namespace);
|
|
13246
|
+
await this.resolveConflict(settings, state, namespace, payload, result.server, out);
|
|
13247
|
+
} else {
|
|
13248
|
+
state.rejectedHashes[namespace] = hash;
|
|
13249
|
+
delete state.pendingMutations[namespace];
|
|
13250
|
+
out.rejected.push(namespace);
|
|
13251
|
+
this.deps.logger?.warn(
|
|
13252
|
+
`cloud-config-sync: server rejected ${namespace} (${result.code}); keeping it local until the config changes.`
|
|
13253
|
+
);
|
|
13254
|
+
}
|
|
13255
|
+
}
|
|
13256
|
+
}
|
|
13257
|
+
async putNamespace(settings, state, namespace, payload, mutationId) {
|
|
13258
|
+
const response = await this.request(
|
|
13259
|
+
settings,
|
|
13260
|
+
"PUT",
|
|
13261
|
+
`/api/sync/v1/config/${encodeURIComponent(namespace)}`,
|
|
13262
|
+
{
|
|
13263
|
+
baseRevision: state.namespaceRevisions[namespace] ?? 0,
|
|
13264
|
+
mutationId,
|
|
13265
|
+
schemaVersion: NAMESPACE_SCHEMA_VERSIONS[namespace] ?? 1,
|
|
13266
|
+
payload
|
|
13267
|
+
},
|
|
13268
|
+
// 409 (conflict) and 422 (contract mismatch) are protocol answers, not failures.
|
|
13269
|
+
[409, 422]
|
|
13270
|
+
);
|
|
13271
|
+
if (response.status === 422) {
|
|
13272
|
+
const code = response.json.error?.code;
|
|
13273
|
+
return { kind: "rejected", code: code ?? "invalid_payload" };
|
|
13274
|
+
}
|
|
13275
|
+
const body = response.json;
|
|
13276
|
+
if (response.status === 409) {
|
|
13277
|
+
return { kind: "conflict", server: body };
|
|
13278
|
+
}
|
|
13279
|
+
state.namespaceRevisions[namespace] = body.revision;
|
|
13280
|
+
state.syncedHashes[namespace] = hashPayload(payload);
|
|
13281
|
+
state.syncedPayloads[namespace] = payload;
|
|
13282
|
+
if (typeof body.cursor === "number") state.cursor = Math.max(state.cursor, body.cursor);
|
|
13283
|
+
delete state.pendingMutations[namespace];
|
|
13284
|
+
delete state.rejectedHashes[namespace];
|
|
13285
|
+
return { kind: "applied" };
|
|
13286
|
+
}
|
|
13287
|
+
/**
|
|
13288
|
+
* Three-way merge at owned-top-level-key granularity: keys unchanged locally since the
|
|
13289
|
+
* last sync adopt the server value; locally-changed keys win and are re-pushed on top
|
|
13290
|
+
* of the server revision with a fresh mutation id.
|
|
13291
|
+
*/
|
|
13292
|
+
async resolveConflict(settings, state, namespace, localPayload, server, out) {
|
|
13293
|
+
const base = state.syncedPayloads[namespace] ?? {};
|
|
13294
|
+
const merged = { ...server.payload };
|
|
13295
|
+
for (const [key, localValue] of Object.entries(localPayload)) {
|
|
13296
|
+
if (!deepEquals(localValue, base[key])) merged[key] = localValue;
|
|
13297
|
+
}
|
|
13298
|
+
await this.deps.writeLocalConfig((config) => applyNamespacePayload(config, namespace, merged));
|
|
13299
|
+
state.namespaceRevisions[namespace] = server.revision;
|
|
13300
|
+
const mutationId = this.uuid();
|
|
13301
|
+
const hash = hashPayload(merged);
|
|
13302
|
+
state.pendingMutations[namespace] = { mutationId, hash };
|
|
13303
|
+
await this.saveState(state);
|
|
13304
|
+
const result = await this.putNamespace(settings, state, namespace, merged, mutationId);
|
|
13305
|
+
if (result.kind === "applied") {
|
|
13306
|
+
out.pushed.push(namespace);
|
|
13307
|
+
} else {
|
|
13308
|
+
this.deps.logger?.warn(
|
|
13309
|
+
`cloud-config-sync: conflict resolution for ${namespace} did not converge (${result.kind}); will retry next pass.`
|
|
13310
|
+
);
|
|
13311
|
+
}
|
|
13312
|
+
}
|
|
13313
|
+
// ── Heartbeat ───────────────────────────────────────────────────────────
|
|
13314
|
+
async heartbeat(settings, state) {
|
|
13315
|
+
await this.request(settings, "POST", "/api/sync/v1/heartbeat", {
|
|
13316
|
+
transport: "poll",
|
|
13317
|
+
syncCursor: state.cursor,
|
|
13318
|
+
namespaceRevisions: state.namespaceRevisions
|
|
13319
|
+
}).catch((error) => {
|
|
13320
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13321
|
+
this.deps.logger?.warn(`cloud-config-sync: heartbeat failed: ${message}`);
|
|
13322
|
+
});
|
|
13323
|
+
}
|
|
13324
|
+
// ── HTTP + state plumbing ───────────────────────────────────────────────
|
|
13325
|
+
async request(settings, method, pathname, body, acceptStatuses = []) {
|
|
13326
|
+
const url = `${settings.url.replace(/\/+$/, "")}${pathname}`;
|
|
13327
|
+
const headers = {
|
|
13328
|
+
Authorization: `Bearer ${settings.token}`,
|
|
13329
|
+
"X-WrongStack-Version": this.deps.appVersion ?? "unknown"
|
|
13330
|
+
};
|
|
13331
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
13332
|
+
const init = {
|
|
13333
|
+
method,
|
|
13334
|
+
headers,
|
|
13335
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
13336
|
+
};
|
|
13337
|
+
if (body !== void 0) init.body = JSON.stringify(body);
|
|
13338
|
+
const response = await this.fetchImpl(url, init);
|
|
13339
|
+
const ok = response.ok || acceptStatuses.includes(response.status);
|
|
13340
|
+
if (!ok) {
|
|
13341
|
+
if (response.status === 401) {
|
|
13342
|
+
throw new Error("unauthorized: the machine token is invalid, expired, or revoked.");
|
|
13343
|
+
}
|
|
13344
|
+
if (response.status === 429) {
|
|
13345
|
+
throw new Error("rate limited by the portal; backing off until the next pass.");
|
|
13346
|
+
}
|
|
13347
|
+
throw new Error(`${method} ${pathname} failed with HTTP ${response.status}.`);
|
|
13348
|
+
}
|
|
13349
|
+
const json = response.status === 204 ? null : await response.json().catch(() => null);
|
|
13350
|
+
return { status: response.status, json };
|
|
13351
|
+
}
|
|
13352
|
+
async loadState() {
|
|
13353
|
+
if (this.state) return this.state;
|
|
13354
|
+
try {
|
|
13355
|
+
const raw = await fs19.readFile(this.deps.statePath, "utf8");
|
|
13356
|
+
const parsed = JSON.parse(raw);
|
|
13357
|
+
this.state = {
|
|
13358
|
+
...structuredClone(EMPTY_STATE),
|
|
13359
|
+
...parsed,
|
|
13360
|
+
version: 1
|
|
13361
|
+
};
|
|
13362
|
+
} catch {
|
|
13363
|
+
this.state = structuredClone(EMPTY_STATE);
|
|
13364
|
+
}
|
|
13365
|
+
return this.state;
|
|
13366
|
+
}
|
|
13367
|
+
async saveState(state) {
|
|
13368
|
+
this.state = state;
|
|
13369
|
+
await fs19.mkdir(path30.dirname(this.deps.statePath), { recursive: true });
|
|
13370
|
+
await atomicWrite(this.deps.statePath, `${JSON.stringify(state, null, 2)}
|
|
13371
|
+
`, {
|
|
13372
|
+
mode: 384
|
|
13373
|
+
});
|
|
13374
|
+
}
|
|
13375
|
+
};
|
|
13376
|
+
function summary(partial) {
|
|
13377
|
+
return {
|
|
13378
|
+
ok: partial.ok ?? false,
|
|
13379
|
+
skipped: partial.skipped ?? false,
|
|
13380
|
+
pulled: partial.pulled ?? [],
|
|
13381
|
+
pushed: partial.pushed ?? [],
|
|
13382
|
+
conflicted: partial.conflicted ?? [],
|
|
13383
|
+
rejected: partial.rejected ?? [],
|
|
13384
|
+
message: partial.message
|
|
13385
|
+
};
|
|
13386
|
+
}
|
|
13387
|
+
|
|
13388
|
+
// src/plugins/cloud-config-sync-plugin.ts
|
|
13389
|
+
init_atomic_write();
|
|
13390
|
+
init_error();
|
|
13391
|
+
var DEFAULT_INTERVAL_SECONDS = 300;
|
|
13392
|
+
var MIN_INTERVAL_SECONDS = 60;
|
|
13393
|
+
function createCloudConfigSyncPlugin(opts) {
|
|
13394
|
+
let engine = null;
|
|
13395
|
+
let timer = null;
|
|
13396
|
+
let passRunning = false;
|
|
13397
|
+
return {
|
|
13398
|
+
name: "wstack-cloud-config-sync",
|
|
13399
|
+
version: "1.0.0",
|
|
13400
|
+
description: "my.wrongstack.com config synchronization (namespaced sync API v1)",
|
|
13401
|
+
apiVersion: "^0.1",
|
|
13402
|
+
capabilities: { slashCommands: true },
|
|
13403
|
+
defaultConfig: {},
|
|
13404
|
+
setup(api) {
|
|
13405
|
+
const rawConfig = api.config;
|
|
13406
|
+
const paths = opts?.paths ?? rawConfig.paths;
|
|
13407
|
+
const configStore = opts?.configStore ?? rawConfig.configStore;
|
|
13408
|
+
const vault = opts?.vault ?? rawConfig.vault;
|
|
13409
|
+
const appVersion = opts?.appVersion ?? rawConfig.appVersion;
|
|
13410
|
+
if (!paths?.configDir || !configStore || !vault) {
|
|
13411
|
+
api.log.warn(
|
|
13412
|
+
"[cloud-config-sync] paths, configStore, or vault not available \u2014 /cloudsync disabled"
|
|
13413
|
+
);
|
|
13414
|
+
return;
|
|
13415
|
+
}
|
|
13416
|
+
const profileConfigPath = path31.join(paths.configDir, "config.json");
|
|
13417
|
+
const statePath = path31.join(paths.configDir, "cloud-sync-state.json");
|
|
13418
|
+
const warn = (msg) => api.log.warn(msg);
|
|
13419
|
+
const readLocalConfig = async () => {
|
|
13420
|
+
const raw = await readJsonObjectFile(profileConfigPath).catch(() => ({}));
|
|
13421
|
+
return decryptConfigSecrets(raw, vault, { warn });
|
|
13422
|
+
};
|
|
13423
|
+
const writeLocalConfig = async (mutator) => {
|
|
13424
|
+
const current = await readLocalConfig();
|
|
13425
|
+
const next = mutator(current);
|
|
13426
|
+
const encrypted = encryptConfigSecrets(next, vault);
|
|
13427
|
+
await atomicWrite(profileConfigPath, `${JSON.stringify(encrypted, null, 2)}
|
|
13428
|
+
`, {
|
|
13429
|
+
mode: 384
|
|
13430
|
+
});
|
|
13431
|
+
configStore.update({ cloudSync: next.cloudSync });
|
|
13432
|
+
};
|
|
13433
|
+
const getSettings = () => {
|
|
13434
|
+
const cfg = configStore.get().cloudSync;
|
|
13435
|
+
if (!cfg?.enabled || !cfg.url || !cfg.token) return null;
|
|
13436
|
+
const token = cfg.token.startsWith("enc:") && vault.decrypt ? vault.decrypt(cfg.token) : cfg.token;
|
|
13437
|
+
return { enabled: true, url: cfg.url, token };
|
|
13438
|
+
};
|
|
13439
|
+
engine = new CloudConfigSync({
|
|
13440
|
+
statePath,
|
|
13441
|
+
readLocalConfig,
|
|
13442
|
+
writeLocalConfig,
|
|
13443
|
+
getSettings,
|
|
13444
|
+
appVersion,
|
|
13445
|
+
logger: { info: (m) => api.log.info(m), warn }
|
|
13446
|
+
});
|
|
13447
|
+
const runPass = async () => {
|
|
13448
|
+
if (!engine) return "Cloud config sync is not initialized.";
|
|
13449
|
+
if (passRunning) return "A sync pass is already running.";
|
|
13450
|
+
passRunning = true;
|
|
13451
|
+
try {
|
|
13452
|
+
const result = await engine.syncOnce();
|
|
13453
|
+
return result.message;
|
|
13454
|
+
} finally {
|
|
13455
|
+
passRunning = false;
|
|
13456
|
+
}
|
|
13457
|
+
};
|
|
13458
|
+
const scheduleLoop = () => {
|
|
13459
|
+
if (timer) {
|
|
13460
|
+
clearInterval(timer);
|
|
13461
|
+
timer = null;
|
|
13462
|
+
}
|
|
13463
|
+
const cfg = configStore.get().cloudSync;
|
|
13464
|
+
if (!cfg?.enabled) return;
|
|
13465
|
+
const seconds = Math.max(
|
|
13466
|
+
MIN_INTERVAL_SECONDS,
|
|
13467
|
+
cfg.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS
|
|
13468
|
+
);
|
|
13469
|
+
timer = setInterval(() => {
|
|
13470
|
+
void runPass().catch((err) => warn(`[cloud-config-sync] ${toErrorMessage(err)}`));
|
|
13471
|
+
}, seconds * 1e3);
|
|
13472
|
+
timer.unref?.();
|
|
13473
|
+
};
|
|
13474
|
+
const setCloudSyncConfig = async (patch) => {
|
|
13475
|
+
await writeLocalConfig((config) => ({
|
|
13476
|
+
...config,
|
|
13477
|
+
cloudSync: { ...config.cloudSync, ...patch }
|
|
13478
|
+
}));
|
|
13479
|
+
scheduleLoop();
|
|
13480
|
+
};
|
|
13481
|
+
api.slashCommands.register(
|
|
13482
|
+
buildCloudSyncCommand(engine, configStore, setCloudSyncConfig, runPass)
|
|
13483
|
+
);
|
|
13484
|
+
scheduleLoop();
|
|
13485
|
+
api.log.info("[cloud-config-sync] loaded \u2014 /cloudsync available.");
|
|
13486
|
+
},
|
|
13487
|
+
teardown(api) {
|
|
13488
|
+
if (timer) {
|
|
13489
|
+
clearInterval(timer);
|
|
13490
|
+
timer = null;
|
|
13491
|
+
}
|
|
13492
|
+
api.slashCommands.unregister("cloudsync");
|
|
13493
|
+
api.log.info("[cloud-config-sync] unloaded");
|
|
13494
|
+
},
|
|
13495
|
+
async health() {
|
|
13496
|
+
return { ok: true, message: "CloudConfigSync ready" };
|
|
13497
|
+
}
|
|
13498
|
+
};
|
|
13499
|
+
}
|
|
13500
|
+
function buildCloudSyncCommand(engine, configStore, setCloudSyncConfig, runPass) {
|
|
13501
|
+
return {
|
|
13502
|
+
name: "cloudsync",
|
|
13503
|
+
description: "Portal config sync: /cloudsync [status|on|off|now|set url|set token]",
|
|
13504
|
+
async run(args, _ctx) {
|
|
13505
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
13506
|
+
const restJoined = rest.join(" ").trim();
|
|
13507
|
+
switch (verb) {
|
|
13508
|
+
case "":
|
|
13509
|
+
case "status":
|
|
13510
|
+
return { message: await engine.statusText() };
|
|
13511
|
+
case "on": {
|
|
13512
|
+
const cfg = configStore.get().cloudSync;
|
|
13513
|
+
if (!cfg?.url || !cfg.token) {
|
|
13514
|
+
return {
|
|
13515
|
+
message: [
|
|
13516
|
+
"Set the portal first:",
|
|
13517
|
+
" /cloudsync set url https://my.wrongstack.com",
|
|
13518
|
+
' /cloudsync set token wst_\u2026 (from the dashboard "Connect client" flow)'
|
|
13519
|
+
].join("\n")
|
|
13520
|
+
};
|
|
13521
|
+
}
|
|
13522
|
+
await setCloudSyncConfig({ enabled: true });
|
|
13523
|
+
const first = await runPass();
|
|
13524
|
+
return { message: `Cloud config sync enabled.
|
|
13525
|
+
${first}` };
|
|
13526
|
+
}
|
|
13527
|
+
case "off": {
|
|
13528
|
+
await setCloudSyncConfig({ enabled: false });
|
|
13529
|
+
return { message: "Cloud config sync disabled. Local config kept as-is." };
|
|
13530
|
+
}
|
|
13531
|
+
case "now": {
|
|
13532
|
+
try {
|
|
13533
|
+
return { message: await runPass() };
|
|
13534
|
+
} catch (err) {
|
|
13535
|
+
return { message: `Sync failed: ${toErrorMessage(err)}` };
|
|
13536
|
+
}
|
|
13537
|
+
}
|
|
13538
|
+
case "set": {
|
|
13539
|
+
const [field, ...valueParts] = restJoined.split(/\s+/);
|
|
13540
|
+
const value = valueParts.join(" ").trim();
|
|
13541
|
+
if (field === "url") {
|
|
13542
|
+
if (!/^https?:\/\//.test(value)) {
|
|
13543
|
+
return { message: "Usage: /cloudsync set url https://my.wrongstack.com" };
|
|
13544
|
+
}
|
|
13545
|
+
await setCloudSyncConfig({ url: value.replace(/\/+$/, "") });
|
|
13546
|
+
return { message: `Portal URL set to ${value}.` };
|
|
13547
|
+
}
|
|
13548
|
+
if (field === "token") {
|
|
13549
|
+
if (!/^wst_[A-Za-z0-9_-]{43}$/.test(value)) {
|
|
13550
|
+
return {
|
|
13551
|
+
message: 'That does not look like a machine token (wst_ + 43 chars). Create one in the dashboard "Connect client" flow.'
|
|
13552
|
+
};
|
|
13553
|
+
}
|
|
13554
|
+
await setCloudSyncConfig({ token: value });
|
|
13555
|
+
return { message: "Machine token stored (encrypted at rest)." };
|
|
13556
|
+
}
|
|
13557
|
+
if (field === "interval") {
|
|
13558
|
+
const seconds = Number.parseInt(value, 10);
|
|
13559
|
+
if (!Number.isFinite(seconds) || seconds < MIN_INTERVAL_SECONDS) {
|
|
13560
|
+
return {
|
|
13561
|
+
message: `Usage: /cloudsync set interval <seconds \u2265 ${MIN_INTERVAL_SECONDS}>`
|
|
13562
|
+
};
|
|
13563
|
+
}
|
|
13564
|
+
await setCloudSyncConfig({ intervalSeconds: seconds });
|
|
13565
|
+
return { message: `Sync interval set to ${seconds}s.` };
|
|
13566
|
+
}
|
|
13567
|
+
return { message: "Usage: /cloudsync set url|token|interval <value>" };
|
|
13568
|
+
}
|
|
13569
|
+
default:
|
|
13570
|
+
return {
|
|
13571
|
+
message: [
|
|
13572
|
+
"/cloudsync \u2014 my.wrongstack.com config sync",
|
|
13573
|
+
"",
|
|
13574
|
+
" /cloudsync status Show sync state",
|
|
13575
|
+
" /cloudsync set url <url> Set the portal URL",
|
|
13576
|
+
" /cloudsync set token <wst_\u2026> Store the machine token (encrypted)",
|
|
13577
|
+
" /cloudsync set interval <sec> Background pass interval",
|
|
13578
|
+
" /cloudsync on Enable + run the first pass",
|
|
13579
|
+
" /cloudsync off Disable background sync",
|
|
13580
|
+
" /cloudsync now Run one pass immediately",
|
|
13581
|
+
"",
|
|
13582
|
+
"Secrets never sync: providers keys, hq binding, and bot tokens stay local."
|
|
13583
|
+
].join("\n")
|
|
13584
|
+
};
|
|
13585
|
+
}
|
|
13586
|
+
}
|
|
13587
|
+
};
|
|
13588
|
+
}
|
|
11957
13589
|
export {
|
|
11958
13590
|
CHIMERA_REVIEW_PROMPT,
|
|
11959
13591
|
DefaultPluginAPI,
|
|
@@ -11961,6 +13593,7 @@ export {
|
|
|
11961
13593
|
buildReviewerModelPool,
|
|
11962
13594
|
createAutoReviewPlugin,
|
|
11963
13595
|
createChimeraPlugin,
|
|
13596
|
+
createCloudConfigSyncPlugin,
|
|
11964
13597
|
createPromptsPlugin,
|
|
11965
13598
|
createSkillsPlugin,
|
|
11966
13599
|
createSyncPlugin,
|
|
@@ -11973,10 +13606,12 @@ export {
|
|
|
11973
13606
|
parseChimeraReviewReport,
|
|
11974
13607
|
parseReviewSeverity,
|
|
11975
13608
|
persistReviewReport,
|
|
13609
|
+
pluginEntryMatchesName,
|
|
11976
13610
|
recordCompletedReview,
|
|
11977
13611
|
recordStartedReview,
|
|
11978
13612
|
redactPluginConfig,
|
|
11979
13613
|
resolvePluginConfig,
|
|
13614
|
+
resolvePluginEnablement,
|
|
11980
13615
|
resolvePluginManifestConfig,
|
|
11981
13616
|
selectRoundRobinReviewerAssignment,
|
|
11982
13617
|
unloadPlugins,
|