@wrongstack/core 0.300.0 → 0.302.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 +2 -2
- package/dist/coordination/index.js +6787 -6447
- 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/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/index.d.ts +8 -7
- package/dist/core/index.js +893 -284
- package/dist/core/instruction-template.d.ts +6 -3
- package/dist/core/next-steps-slot.d.ts +88 -0
- package/dist/core/system-prompt-builder.d.ts +5 -3
- 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 +9797 -8898
- package/dist/design/index.js +11 -3
- package/dist/execution/auto-compaction-middleware.d.ts +17 -0
- package/dist/execution/autonomy-brain.d.ts +8 -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 +37 -2
- package/dist/execution/council-orchestrator.d.ts +2 -2
- package/dist/execution/index.d.ts +11 -10
- package/dist/execution/index.js +5179 -4078
- 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/goal/index.js +11 -3
- package/dist/hooks/index.js +42 -9
- package/dist/hooks/runner.d.ts +12 -1
- package/dist/hq/index.js +41 -17
- package/dist/hq/publisher.d.ts +21 -3
- package/dist/index.d.ts +21 -20
- package/dist/index.js +10901 -8067
- package/dist/infrastructure/index.js +108 -21
- 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 +17 -0
- package/dist/models/provider-credentials.d.ts +54 -0
- package/dist/plugin/index.d.ts +1 -0
- package/dist/plugin/index.js +2024 -502
- 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 +1 -1
- package/dist/security/index.js +9 -2
- package/dist/security/readonly-permission-policy.d.ts +20 -0
- package/dist/session-registry-atomic-file.d.ts +32 -5
- package/dist/session-registry-types.d.ts +17 -0
- package/dist/session-registry.d.ts +1 -1
- 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/index.d.ts +2 -1
- package/dist/storage/index.js +854 -66
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +511 -234
- 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/hooks.d.ts +14 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +1 -0
- package/dist/types/multi-agent.d.ts +2 -0
- package/dist/types/one-shot-llm.d.ts +16 -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/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 +10 -0
- package/instructions/system-pro.md +23 -14
- package/instructions/system.md +22 -13
- 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
|
}
|
|
@@ -2209,8 +2215,8 @@ function definePlugin(metadata, factory) {
|
|
|
2209
2215
|
// src/plugins/auto-review-plugin.ts
|
|
2210
2216
|
import { spawn as spawn3 } from "node:child_process";
|
|
2211
2217
|
import { createHash as createHash7 } from "node:crypto";
|
|
2212
|
-
import * as
|
|
2213
|
-
import * as
|
|
2218
|
+
import * as fsp2 from "node:fs/promises";
|
|
2219
|
+
import * as path15 from "node:path";
|
|
2214
2220
|
|
|
2215
2221
|
// src/core/model-availability-calendar.ts
|
|
2216
2222
|
function logicalCalendarTarget(providerId, model) {
|
|
@@ -2572,14 +2578,327 @@ var FREEZER_EMPTY = Object.freeze([]);
|
|
|
2572
2578
|
init_error();
|
|
2573
2579
|
|
|
2574
2580
|
// src/plugins/review-claim-registry.ts
|
|
2575
|
-
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();
|
|
2576
2721
|
var claimsByEventBus = /* @__PURE__ */ new WeakMap();
|
|
2577
2722
|
var startedReviews = /* @__PURE__ */ new WeakMap();
|
|
2723
|
+
var pendingStartedReviews = /* @__PURE__ */ new WeakMap();
|
|
2578
2724
|
function fingerprint(content) {
|
|
2579
2725
|
return createHash("sha256").update(content).digest("hex");
|
|
2580
2726
|
}
|
|
2727
|
+
function normalizeKeyPart(p) {
|
|
2728
|
+
const forward = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2729
|
+
return process.platform === "win32" ? forward.toLowerCase() : forward;
|
|
2730
|
+
}
|
|
2581
2731
|
function claimKey(cwd, filePath) {
|
|
2582
|
-
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
|
+
);
|
|
2583
2902
|
}
|
|
2584
2903
|
function ledgerFor(events) {
|
|
2585
2904
|
let ledger = claimsByEventBus.get(events);
|
|
@@ -2589,68 +2908,162 @@ function ledgerFor(events) {
|
|
|
2589
2908
|
}
|
|
2590
2909
|
return ledger;
|
|
2591
2910
|
}
|
|
2592
|
-
function
|
|
2911
|
+
function claimFilesInMemory(events, cwd, files, ttlMs) {
|
|
2593
2912
|
const ledger = ledgerFor(events);
|
|
2913
|
+
const now = Date.now();
|
|
2594
2914
|
const claims = [];
|
|
2595
2915
|
for (const file of files) {
|
|
2596
2916
|
const key = claimKey(cwd, file.path);
|
|
2597
2917
|
const fileFingerprint = fingerprint(file.content);
|
|
2598
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
|
+
}
|
|
2599
2928
|
if (!activeFingerprints) {
|
|
2600
|
-
activeFingerprints = /* @__PURE__ */ new
|
|
2929
|
+
activeFingerprints = /* @__PURE__ */ new Map();
|
|
2601
2930
|
ledger.set(key, activeFingerprints);
|
|
2602
2931
|
}
|
|
2603
2932
|
if (activeFingerprints.has(fileFingerprint)) continue;
|
|
2604
|
-
activeFingerprints.
|
|
2605
|
-
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 });
|
|
2606
2935
|
}
|
|
2607
2936
|
return claims;
|
|
2608
2937
|
}
|
|
2609
|
-
function
|
|
2938
|
+
function releaseClaimsInMemory(events, claims) {
|
|
2610
2939
|
const ledger = ledgerFor(events);
|
|
2611
2940
|
for (const claim of claims) {
|
|
2612
2941
|
const activeFingerprints = ledger.get(claim.key);
|
|
2613
2942
|
if (!activeFingerprints) continue;
|
|
2614
|
-
activeFingerprints.
|
|
2943
|
+
if (activeFingerprints.get(claim.fingerprint)?.ts === claim.ts) {
|
|
2944
|
+
activeFingerprints.delete(claim.fingerprint);
|
|
2945
|
+
}
|
|
2615
2946
|
if (activeFingerprints.size === 0) ledger.delete(claim.key);
|
|
2616
2947
|
}
|
|
2617
2948
|
}
|
|
2618
|
-
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) {
|
|
2619
3001
|
if (!payload || typeof payload !== "object") return;
|
|
3002
|
+
const bundle = payload;
|
|
3003
|
+
if (startedReviews.has(bundle) || pendingStartedReviews.has(bundle)) return;
|
|
2620
3004
|
const candidate = payload;
|
|
2621
3005
|
if (typeof candidate.cwd !== "string" || !Array.isArray(candidate.files)) return;
|
|
3006
|
+
const cwd = candidate.cwd;
|
|
2622
3007
|
const files = candidate.files.filter(
|
|
2623
3008
|
(file) => Boolean(file) && typeof file === "object" && typeof file.path === "string" && typeof file.content === "string"
|
|
2624
3009
|
);
|
|
2625
|
-
const
|
|
2626
|
-
|
|
2627
|
-
|
|
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
|
+
}
|
|
2628
3028
|
}
|
|
2629
3029
|
}
|
|
2630
|
-
function recordCompletedReview(events, payload) {
|
|
3030
|
+
async function recordCompletedReview(events, payload) {
|
|
2631
3031
|
if (!payload || typeof payload !== "object") return;
|
|
2632
3032
|
const bundle = payload.bundle;
|
|
2633
3033
|
if (!bundle || typeof bundle !== "object") return;
|
|
2634
|
-
|
|
3034
|
+
let startedReview = startedReviews.get(bundle);
|
|
3035
|
+
if (!startedReview) {
|
|
3036
|
+
const pending = pendingStartedReviews.get(bundle);
|
|
3037
|
+
if (pending) startedReview = await pending ?? void 0;
|
|
3038
|
+
}
|
|
2635
3039
|
if (!startedReview || startedReview.events !== events) return;
|
|
2636
|
-
releaseClaims(events, startedReview.claims);
|
|
3040
|
+
await releaseClaims(events, startedReview.claims, startedReview.storeDir);
|
|
2637
3041
|
startedReviews.delete(bundle);
|
|
2638
3042
|
}
|
|
2639
|
-
function emitReviewIfChanged(api, bundle) {
|
|
2640
|
-
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);
|
|
2641
3045
|
if (claims.length === 0) return null;
|
|
2642
|
-
const
|
|
3046
|
+
const claimed = new Set(claims.map((claim) => `${claim.key}\0${claim.fingerprint}`));
|
|
2643
3047
|
const claimedBundle = {
|
|
2644
3048
|
...bundle,
|
|
2645
|
-
|
|
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
|
+
)
|
|
2646
3055
|
};
|
|
2647
|
-
startedReviews.set(claimedBundle, {
|
|
3056
|
+
startedReviews.set(claimedBundle, {
|
|
3057
|
+
events: api.events,
|
|
3058
|
+
storeDir,
|
|
3059
|
+
claims
|
|
3060
|
+
});
|
|
2648
3061
|
try {
|
|
2649
3062
|
api.emitCustom("chimera.review_needed", claimedBundle);
|
|
2650
3063
|
return claimedBundle;
|
|
2651
3064
|
} catch (err) {
|
|
2652
3065
|
startedReviews.delete(claimedBundle);
|
|
2653
|
-
releaseClaims(api.events, claims);
|
|
3066
|
+
await releaseClaims(api.events, claims, storeDir);
|
|
2654
3067
|
throw err;
|
|
2655
3068
|
}
|
|
2656
3069
|
}
|
|
@@ -2661,27 +3074,30 @@ import { spawn as spawn2 } from "node:child_process";
|
|
|
2661
3074
|
// src/chronicle/project-access.ts
|
|
2662
3075
|
import * as fsPromises from "node:fs/promises";
|
|
2663
3076
|
import * as os4 from "node:os";
|
|
2664
|
-
import * as
|
|
3077
|
+
import * as path14 from "node:path";
|
|
2665
3078
|
|
|
2666
3079
|
// src/utils/wstack-paths.ts
|
|
2667
3080
|
import { createHash as createHash2 } from "node:crypto";
|
|
2668
3081
|
import * as fs from "node:fs";
|
|
2669
3082
|
import * as os from "node:os";
|
|
2670
|
-
import * as
|
|
3083
|
+
import * as path2 from "node:path";
|
|
2671
3084
|
function canonicalProjectRoot(absRoot) {
|
|
2672
|
-
const checkoutRoot =
|
|
2673
|
-
const dotGit =
|
|
3085
|
+
const checkoutRoot = path2.resolve(absRoot);
|
|
3086
|
+
const dotGit = path2.join(checkoutRoot, ".git");
|
|
2674
3087
|
try {
|
|
2675
3088
|
if (!fs.statSync(dotGit).isFile()) return checkoutRoot;
|
|
2676
3089
|
const gitDirLine = fs.readFileSync(dotGit, "utf8").trim();
|
|
2677
3090
|
const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
|
|
2678
3091
|
if (!match?.[1]) return checkoutRoot;
|
|
2679
|
-
const gitDir =
|
|
2680
|
-
const commonDirFile =
|
|
3092
|
+
const gitDir = path2.resolve(checkoutRoot, match[1].trim());
|
|
3093
|
+
const commonDirFile = path2.join(gitDir, "commondir");
|
|
2681
3094
|
if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
|
|
2682
|
-
const commonDir =
|
|
2683
|
-
|
|
2684
|
-
|
|
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);
|
|
2685
3101
|
} catch {
|
|
2686
3102
|
return checkoutRoot;
|
|
2687
3103
|
}
|
|
@@ -2691,7 +3107,7 @@ function projectHash(absRoot) {
|
|
|
2691
3107
|
}
|
|
2692
3108
|
function projectSlug(absRoot) {
|
|
2693
3109
|
const identityRoot = canonicalProjectRoot(absRoot);
|
|
2694
|
-
const base = slugify(
|
|
3110
|
+
const base = slugify(path2.basename(identityRoot));
|
|
2695
3111
|
const hash = createHash2("sha256").update(identityRoot).digest("hex").slice(0, 6);
|
|
2696
3112
|
return `${base}-${hash}`;
|
|
2697
3113
|
}
|
|
@@ -2702,9 +3118,9 @@ function safeProfileName(name) {
|
|
|
2702
3118
|
const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
|
|
2703
3119
|
return safe || "default";
|
|
2704
3120
|
}
|
|
2705
|
-
function
|
|
3121
|
+
function bootstrapProfileName(globalRoot) {
|
|
2706
3122
|
try {
|
|
2707
|
-
const parsed = JSON.parse(fs.readFileSync(
|
|
3123
|
+
const parsed = JSON.parse(fs.readFileSync(path2.join(globalRoot, "config.json"), "utf8"));
|
|
2708
3124
|
return safeProfileName(
|
|
2709
3125
|
typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
|
|
2710
3126
|
);
|
|
@@ -2714,17 +3130,17 @@ function activeProfileName(globalRoot) {
|
|
|
2714
3130
|
}
|
|
2715
3131
|
function wstackGlobalRoot() {
|
|
2716
3132
|
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
2717
|
-
if (fromEnv && fromEnv.trim().length > 0) return
|
|
2718
|
-
return
|
|
3133
|
+
if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
|
|
3134
|
+
return path2.join(os.homedir(), ".wrongstack");
|
|
2719
3135
|
}
|
|
2720
3136
|
function resolveWstackPaths(opts) {
|
|
2721
|
-
const globalRoot = opts.globalRoot ?? (opts.userHome ?
|
|
3137
|
+
const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
2722
3138
|
const homeDir = opts.userHome ?? os.homedir();
|
|
2723
|
-
const profileName = safeProfileName(opts.profileName ??
|
|
2724
|
-
const profileDir =
|
|
3139
|
+
const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
|
|
3140
|
+
const profileDir = path2.join(globalRoot, "profiles", profileName);
|
|
2725
3141
|
const hash = projectHash(opts.projectRoot);
|
|
2726
3142
|
const slug = projectSlug(opts.projectRoot);
|
|
2727
|
-
const projectDir =
|
|
3143
|
+
const projectDir = path2.join(globalRoot, "projects", slug);
|
|
2728
3144
|
return {
|
|
2729
3145
|
globalRoot,
|
|
2730
3146
|
profileName,
|
|
@@ -2732,79 +3148,84 @@ function resolveWstackPaths(opts) {
|
|
|
2732
3148
|
projectRoot: opts.projectRoot,
|
|
2733
3149
|
homeDir,
|
|
2734
3150
|
configDir: profileDir,
|
|
2735
|
-
globalConfig:
|
|
2736
|
-
profilesDir:
|
|
3151
|
+
globalConfig: path2.join(globalRoot, "config.json"),
|
|
3152
|
+
profilesDir: path2.join(globalRoot, "profiles"),
|
|
2737
3153
|
profileConfig: (name) => {
|
|
2738
|
-
return
|
|
3154
|
+
return path2.join(globalRoot, "profiles", safeProfileName(name), "config.json");
|
|
2739
3155
|
},
|
|
2740
3156
|
profileStatuslineConfig: (name) => {
|
|
2741
|
-
return
|
|
3157
|
+
return path2.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
|
|
2742
3158
|
},
|
|
2743
3159
|
profileModeConfig: (name) => {
|
|
2744
|
-
return
|
|
3160
|
+
return path2.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
|
|
2745
3161
|
},
|
|
2746
3162
|
profileProviderStatus: (name) => {
|
|
2747
|
-
return
|
|
3163
|
+
return path2.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
|
|
2748
3164
|
},
|
|
2749
3165
|
profileUpdateCache: (name) => {
|
|
2750
|
-
return
|
|
3166
|
+
return path2.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
|
|
2751
3167
|
},
|
|
2752
|
-
secretsKey:
|
|
2753
|
-
globalMemory:
|
|
2754
|
-
globalSkills:
|
|
2755
|
-
globalClaudeSkills:
|
|
2756
|
-
globalDesignKits:
|
|
2757
|
-
globalPrompts:
|
|
2758
|
-
globalInstructions:
|
|
2759
|
-
promptUsage:
|
|
2760
|
-
cacheDir:
|
|
2761
|
-
modelsCache:
|
|
2762
|
-
modelsOverlayCache:
|
|
2763
|
-
historyFile:
|
|
2764
|
-
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"),
|
|
2765
3181
|
projectDir,
|
|
2766
|
-
projectCodebaseIndex:
|
|
2767
|
-
projectMemory:
|
|
2768
|
-
projectSessions:
|
|
2769
|
-
projectTrust:
|
|
2770
|
-
projectMeta:
|
|
2771
|
-
projectLocalConfig:
|
|
2772
|
-
inProjectConfig:
|
|
2773
|
-
inProjectAgentsFile:
|
|
2774
|
-
inProjectSkills:
|
|
2775
|
-
inProjectClaudeSkills:
|
|
2776
|
-
inProjectPrompts:
|
|
2777
|
-
inProjectInstructions:
|
|
2778
|
-
inProjectDesignKits:
|
|
2779
|
-
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"),
|
|
2780
3196
|
projectHash: hash,
|
|
2781
3197
|
projectSlug: slug,
|
|
2782
|
-
projectGoal:
|
|
2783
|
-
projectInputHistory:
|
|
2784
|
-
projectSpecs:
|
|
2785
|
-
projectTaskGraphs:
|
|
2786
|
-
projectSddSession:
|
|
2787
|
-
projectPlan:
|
|
2788
|
-
projectAutophase:
|
|
2789
|
-
projectSddBoards:
|
|
2790
|
-
projectRequirementIntakes:
|
|
2791
|
-
syncConfig:
|
|
2792
|
-
configHistoryDir:
|
|
2793
|
-
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
|
+
}
|
|
2794
3215
|
};
|
|
2795
3216
|
}
|
|
2796
3217
|
|
|
2797
3218
|
// src/chronicle/identity.ts
|
|
2798
3219
|
import { createHash as createHash3 } from "node:crypto";
|
|
2799
3220
|
import * as os2 from "node:os";
|
|
2800
|
-
import * as
|
|
3221
|
+
import * as path3 from "node:path";
|
|
2801
3222
|
function resolveChronicleRuntimeLocation(input) {
|
|
2802
3223
|
const day = (input.now ?? /* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2803
3224
|
return {
|
|
2804
|
-
installationId: stableId("installation",
|
|
3225
|
+
installationId: stableId("installation", path3.resolve(input.globalRoot)),
|
|
2805
3226
|
machineId: stableId("machine", `${os2.hostname()}\0${os2.platform()}\0${os2.arch()}`),
|
|
2806
3227
|
projectId: input.projectId,
|
|
2807
|
-
chronicleDirectory:
|
|
3228
|
+
chronicleDirectory: path3.join(input.projectDir, "chronicle"),
|
|
2808
3229
|
day
|
|
2809
3230
|
};
|
|
2810
3231
|
}
|
|
@@ -2815,10 +3236,10 @@ function stableId(prefix, value) {
|
|
|
2815
3236
|
// src/chronicle/journal.ts
|
|
2816
3237
|
init_file_permissions();
|
|
2817
3238
|
init_atomic_write();
|
|
2818
|
-
import { randomUUID } from "node:crypto";
|
|
3239
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2819
3240
|
import { createReadStream } from "node:fs";
|
|
2820
3241
|
import * as fs2 from "node:fs/promises";
|
|
2821
|
-
import * as
|
|
3242
|
+
import * as path5 from "node:path";
|
|
2822
3243
|
import { createInterface } from "node:readline";
|
|
2823
3244
|
|
|
2824
3245
|
// src/chronicle/event-hash.ts
|
|
@@ -2841,14 +3262,14 @@ function chronicleEventHash(event) {
|
|
|
2841
3262
|
}
|
|
2842
3263
|
|
|
2843
3264
|
// src/chronicle/partition-filename.ts
|
|
2844
|
-
import * as
|
|
3265
|
+
import * as path4 from "node:path";
|
|
2845
3266
|
var PARTITION_FILE_PATTERN = /^(.*\.events)(?:\.(\d{5}))?\.jsonl$/;
|
|
2846
3267
|
function partitionFamily(filePath) {
|
|
2847
|
-
const match = PARTITION_FILE_PATTERN.exec(
|
|
2848
|
-
return
|
|
3268
|
+
const match = PARTITION_FILE_PATTERN.exec(path4.basename(filePath));
|
|
3269
|
+
return path4.join(path4.dirname(filePath), match?.[1] ?? path4.basename(filePath));
|
|
2849
3270
|
}
|
|
2850
3271
|
function partitionSequence(filePath) {
|
|
2851
|
-
const match = PARTITION_FILE_PATTERN.exec(
|
|
3272
|
+
const match = PARTITION_FILE_PATTERN.exec(path4.basename(filePath));
|
|
2852
3273
|
return Number(match?.[2] ?? 0);
|
|
2853
3274
|
}
|
|
2854
3275
|
function comparePartitionPaths(left, right) {
|
|
@@ -2897,10 +3318,10 @@ var ChronicleJournal = class {
|
|
|
2897
3318
|
lastHash = GENESIS_HASH;
|
|
2898
3319
|
lastAutoPurgeAt = 0;
|
|
2899
3320
|
constructor(options) {
|
|
2900
|
-
this.basePath =
|
|
3321
|
+
this.basePath = path5.resolve(options.filePath);
|
|
2901
3322
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2902
3323
|
this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
|
|
2903
|
-
this.idFactory = options.idFactory ??
|
|
3324
|
+
this.idFactory = options.idFactory ?? randomUUID2;
|
|
2904
3325
|
this.maxPending = Math.max(1, options.maxPending ?? 1e5);
|
|
2905
3326
|
this.batchWindowMs = Math.max(0, options.batchWindowMs ?? 5);
|
|
2906
3327
|
this.maxPartitionSizeBytes = options.maxPartitionSizeBytes ?? DEFAULT_MAX_PARTITION_BYTES;
|
|
@@ -2970,14 +3391,14 @@ var ChronicleJournal = class {
|
|
|
2970
3391
|
}
|
|
2971
3392
|
await this.refreshStateFromDisk();
|
|
2972
3393
|
const cutoff = Date.now() - options.retentionDays * 864e5;
|
|
2973
|
-
const activePath =
|
|
3394
|
+
const activePath = path5.resolve(this.path);
|
|
2974
3395
|
const errors = [];
|
|
2975
3396
|
let dc = 0, db = 0, sc = 0;
|
|
2976
3397
|
const suppliedFiles = options.files === void 0 ? await collectJournalPartitions(this.basePath) : [...options.files];
|
|
2977
3398
|
const eligible = /* @__PURE__ */ new Set();
|
|
2978
3399
|
for (const suppliedPath of new Set(suppliedFiles)) {
|
|
2979
|
-
const file =
|
|
2980
|
-
if (!isJournalPartition(file,
|
|
3400
|
+
const file = path5.resolve(suppliedPath);
|
|
3401
|
+
if (!isJournalPartition(file, path5.dirname(this.basePath), this.basePath)) {
|
|
2981
3402
|
errors.push({
|
|
2982
3403
|
file: suppliedPath,
|
|
2983
3404
|
reason: "not a Chronicle journal partition in this journal directory"
|
|
@@ -3198,34 +3619,34 @@ var ChronicleJournal = class {
|
|
|
3198
3619
|
}
|
|
3199
3620
|
};
|
|
3200
3621
|
function rotatedPath(basePath, index) {
|
|
3201
|
-
const dir =
|
|
3202
|
-
const ext =
|
|
3203
|
-
const base =
|
|
3204
|
-
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}`);
|
|
3205
3626
|
}
|
|
3206
3627
|
async function collectPartitions(basePath) {
|
|
3207
|
-
const dir =
|
|
3208
|
-
const ext =
|
|
3209
|
-
const base =
|
|
3628
|
+
const dir = path5.dirname(basePath);
|
|
3629
|
+
const ext = path5.extname(basePath);
|
|
3630
|
+
const base = path5.basename(basePath, ext);
|
|
3210
3631
|
const pattern = new RegExp(`^${escapeRegex(base)}(?:\\.\\d{5})?${escapeRegex(ext)}$`);
|
|
3211
3632
|
const result = [];
|
|
3212
3633
|
try {
|
|
3213
3634
|
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
3214
3635
|
for (const entry of entries)
|
|
3215
|
-
if (entry.isFile() && pattern.test(entry.name)) result.push(
|
|
3636
|
+
if (entry.isFile() && pattern.test(entry.name)) result.push(path5.join(dir, entry.name));
|
|
3216
3637
|
} catch {
|
|
3217
3638
|
}
|
|
3218
|
-
const baseFile =
|
|
3639
|
+
const baseFile = path5.join(dir, base + ext);
|
|
3219
3640
|
const rotated = result.filter((file) => file !== baseFile).sort((left, right) => parseIndex(left, base, ext) - parseIndex(right, base, ext));
|
|
3220
3641
|
return [baseFile, ...rotated];
|
|
3221
3642
|
}
|
|
3222
3643
|
async function collectJournalPartitions(basePath) {
|
|
3223
|
-
const directory =
|
|
3644
|
+
const directory = path5.dirname(basePath);
|
|
3224
3645
|
const result = [];
|
|
3225
3646
|
try {
|
|
3226
3647
|
const entries = await fs2.readdir(directory, { withFileTypes: true });
|
|
3227
3648
|
for (const entry of entries) {
|
|
3228
|
-
const file =
|
|
3649
|
+
const file = path5.join(directory, entry.name);
|
|
3229
3650
|
if (entry.isFile() && isJournalPartition(file, directory, basePath)) result.push(file);
|
|
3230
3651
|
}
|
|
3231
3652
|
} catch {
|
|
@@ -3233,13 +3654,13 @@ async function collectJournalPartitions(basePath) {
|
|
|
3233
3654
|
return result.sort(comparePartitionPaths);
|
|
3234
3655
|
}
|
|
3235
3656
|
function isJournalPartition(filePath, directory, basePath) {
|
|
3236
|
-
if (
|
|
3237
|
-
const fileName =
|
|
3657
|
+
if (path5.dirname(path5.resolve(filePath)) !== path5.resolve(directory)) return false;
|
|
3658
|
+
const fileName = path5.basename(filePath);
|
|
3238
3659
|
if (!basePath) return false;
|
|
3239
|
-
const baseName =
|
|
3660
|
+
const baseName = path5.basename(basePath);
|
|
3240
3661
|
const dailyFamily = /^\d{4}-\d{2}-\d{2}\.events(?:\.\d{5})?\.jsonl$/;
|
|
3241
3662
|
if (/^\d{4}-\d{2}-\d{2}\.events\.jsonl$/.test(baseName)) return dailyFamily.test(fileName);
|
|
3242
|
-
return partitionFamilyBase(
|
|
3663
|
+
return partitionFamilyBase(path5.resolve(filePath)) === partitionFamilyBase(path5.resolve(basePath));
|
|
3243
3664
|
}
|
|
3244
3665
|
function groupPartitionsByFamily(files) {
|
|
3245
3666
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -3363,12 +3784,12 @@ function isHash(value) {
|
|
|
3363
3784
|
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
3364
3785
|
}
|
|
3365
3786
|
function parseIndex(filePath, base, ext) {
|
|
3366
|
-
const suffix =
|
|
3787
|
+
const suffix = path5.basename(filePath).slice(base.length + 1, -ext.length);
|
|
3367
3788
|
return suffix ? parseInt(suffix, 10) : 0;
|
|
3368
3789
|
}
|
|
3369
3790
|
function partitionIndex(filePath, basePath) {
|
|
3370
|
-
const ext =
|
|
3371
|
-
return parseIndex(filePath,
|
|
3791
|
+
const ext = path5.extname(basePath);
|
|
3792
|
+
return parseIndex(filePath, path5.basename(basePath, ext), ext);
|
|
3372
3793
|
}
|
|
3373
3794
|
function escapeRegex(text) {
|
|
3374
3795
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -3382,8 +3803,8 @@ async function readLastEntryState(filePath) {
|
|
|
3382
3803
|
throw error;
|
|
3383
3804
|
}
|
|
3384
3805
|
try {
|
|
3385
|
-
const
|
|
3386
|
-
const size =
|
|
3806
|
+
const stat8 = await handle.stat();
|
|
3807
|
+
const size = stat8.size;
|
|
3387
3808
|
let position = size, suffix = "";
|
|
3388
3809
|
while (position > 0) {
|
|
3389
3810
|
const length = Math.min(65536, position);
|
|
@@ -3400,14 +3821,14 @@ async function readLastEntryState(filePath) {
|
|
|
3400
3821
|
return {
|
|
3401
3822
|
entry: JSON.parse(trimmed),
|
|
3402
3823
|
size,
|
|
3403
|
-
birthtimeMs:
|
|
3824
|
+
birthtimeMs: stat8.birthtimeMs
|
|
3404
3825
|
};
|
|
3405
3826
|
} catch {
|
|
3406
3827
|
}
|
|
3407
3828
|
}
|
|
3408
3829
|
suffix = lines[0] ?? "";
|
|
3409
3830
|
}
|
|
3410
|
-
return { size, birthtimeMs:
|
|
3831
|
+
return { size, birthtimeMs: stat8.birthtimeMs };
|
|
3411
3832
|
} finally {
|
|
3412
3833
|
await handle.close();
|
|
3413
3834
|
}
|
|
@@ -3425,7 +3846,7 @@ async function* streamEntriesStrict(filePath) {
|
|
|
3425
3846
|
try {
|
|
3426
3847
|
entry = JSON.parse(trimmed);
|
|
3427
3848
|
} catch {
|
|
3428
|
-
throw new Error(`invalid JSON at line ${lineNumber} in ${
|
|
3849
|
+
throw new Error(`invalid JSON at line ${lineNumber} in ${path5.basename(filePath)}`);
|
|
3429
3850
|
}
|
|
3430
3851
|
yield entry;
|
|
3431
3852
|
}
|
|
@@ -3450,7 +3871,7 @@ function errorMessage(error) {
|
|
|
3450
3871
|
init_atomic_write();
|
|
3451
3872
|
import * as fs5 from "node:fs/promises";
|
|
3452
3873
|
import { createRequire } from "node:module";
|
|
3453
|
-
import * as
|
|
3874
|
+
import * as path8 from "node:path";
|
|
3454
3875
|
|
|
3455
3876
|
// src/utils/sqlite-warning.ts
|
|
3456
3877
|
var SQLITE_EXPERIMENTAL_WARNING_RE = /sqlite is an experimental feature/i;
|
|
@@ -3479,13 +3900,13 @@ function withSqliteExperimentalWarningSuppressed(run) {
|
|
|
3479
3900
|
import { createHash as createHash5 } from "node:crypto";
|
|
3480
3901
|
import { createReadStream as createReadStream2 } from "node:fs";
|
|
3481
3902
|
import * as fs4 from "node:fs/promises";
|
|
3482
|
-
import * as
|
|
3903
|
+
import * as path7 from "node:path";
|
|
3483
3904
|
import { createInterface as createInterface2 } from "node:readline";
|
|
3484
3905
|
|
|
3485
3906
|
// src/chronicle/partition-range-cache.ts
|
|
3486
3907
|
init_atomic_write();
|
|
3487
3908
|
import * as fs3 from "node:fs/promises";
|
|
3488
|
-
import * as
|
|
3909
|
+
import * as path6 from "node:path";
|
|
3489
3910
|
var CACHE_VERSION = 1;
|
|
3490
3911
|
function isRange(value) {
|
|
3491
3912
|
if (!value || typeof value !== "object") return false;
|
|
@@ -3495,14 +3916,14 @@ function isRange(value) {
|
|
|
3495
3916
|
var ChroniclePartitionRangeCache = class {
|
|
3496
3917
|
constructor(directory) {
|
|
3497
3918
|
this.directory = directory;
|
|
3498
|
-
this.cachePath =
|
|
3919
|
+
this.cachePath = path6.join(directory, "range-cache.json");
|
|
3499
3920
|
}
|
|
3500
3921
|
directory;
|
|
3501
3922
|
cachePath;
|
|
3502
3923
|
entries;
|
|
3503
3924
|
dirty = false;
|
|
3504
3925
|
toKey(filePath) {
|
|
3505
|
-
return
|
|
3926
|
+
return path6.relative(this.directory, filePath).split(path6.sep).join("/");
|
|
3506
3927
|
}
|
|
3507
3928
|
async load() {
|
|
3508
3929
|
if (this.entries) return this.entries;
|
|
@@ -3603,7 +4024,7 @@ var ChronicleQueryEngine = class _ChronicleQueryEngine {
|
|
|
3603
4024
|
/** The one file (if any) that could still be actively appended to — never cache-checked or skipped. */
|
|
3604
4025
|
activeFile;
|
|
3605
4026
|
static async fromDirectory(directory, options) {
|
|
3606
|
-
const resolved =
|
|
4027
|
+
const resolved = path7.resolve(directory);
|
|
3607
4028
|
const files = await findPartitions(resolved);
|
|
3608
4029
|
const rangeCache = options?.rangeCache ?? new ChroniclePartitionRangeCache(resolved);
|
|
3609
4030
|
return new _ChronicleQueryEngine(
|
|
@@ -4000,7 +4421,7 @@ async function findPartitions(root) {
|
|
|
4000
4421
|
return;
|
|
4001
4422
|
}
|
|
4002
4423
|
for (const entry of entries) {
|
|
4003
|
-
const full =
|
|
4424
|
+
const full = path7.join(directory, entry.name);
|
|
4004
4425
|
if (entry.isDirectory()) await visit(full);
|
|
4005
4426
|
else if (entry.isFile() && PARTITION_FILE_PATTERN.test(entry.name)) result.push(full);
|
|
4006
4427
|
}
|
|
@@ -4187,7 +4608,7 @@ async function resolveSnapshotFiles(files, snapshot) {
|
|
|
4187
4608
|
}));
|
|
4188
4609
|
}
|
|
4189
4610
|
function fileId(file) {
|
|
4190
|
-
return createHash5("sha256").update(
|
|
4611
|
+
return createHash5("sha256").update(path7.resolve(file), "utf8").digest("base64url");
|
|
4191
4612
|
}
|
|
4192
4613
|
function stableStringify2(value) {
|
|
4193
4614
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
@@ -4299,8 +4720,8 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
|
|
|
4299
4720
|
directory;
|
|
4300
4721
|
dbPath;
|
|
4301
4722
|
constructor(directory) {
|
|
4302
|
-
this.directory =
|
|
4303
|
-
this.dbPath =
|
|
4723
|
+
this.directory = path8.resolve(directory);
|
|
4724
|
+
this.dbPath = path8.join(this.directory, "metrics.db");
|
|
4304
4725
|
const Database = loadDatabaseSync();
|
|
4305
4726
|
this.db = new Database(this.dbPath);
|
|
4306
4727
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
@@ -4325,7 +4746,7 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
|
|
|
4325
4746
|
const files = await findChroniclePartitions(this.directory);
|
|
4326
4747
|
const offsets = this.loadOffsets();
|
|
4327
4748
|
for (const file of files) {
|
|
4328
|
-
const key = normalizeKey(
|
|
4749
|
+
const key = normalizeKey(path8.relative(this.directory, file));
|
|
4329
4750
|
const consumed = offsets.get(key) ?? 0;
|
|
4330
4751
|
const ingested = await this.ingestFile(file, key, consumed, result);
|
|
4331
4752
|
if (ingested) result.sourceFiles++;
|
|
@@ -4695,7 +5116,7 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
|
|
|
4695
5116
|
}
|
|
4696
5117
|
pruneOffsets(existingFiles) {
|
|
4697
5118
|
const keep = new Set(
|
|
4698
|
-
existingFiles.map((file) => normalizeKey(
|
|
5119
|
+
existingFiles.map((file) => normalizeKey(path8.relative(this.directory, file)))
|
|
4699
5120
|
);
|
|
4700
5121
|
for (const row of this.db.prepare("SELECT file FROM ingest_state").all()) {
|
|
4701
5122
|
if (!keep.has(row.file))
|
|
@@ -4992,7 +5413,7 @@ function normalizePathKey(value) {
|
|
|
4992
5413
|
|
|
4993
5414
|
// src/chronicle/legacy-journal-import.ts
|
|
4994
5415
|
import * as fs6 from "node:fs/promises";
|
|
4995
|
-
import * as
|
|
5416
|
+
import * as path9 from "node:path";
|
|
4996
5417
|
var ChronicleImportError = class extends Error {
|
|
4997
5418
|
constructor(message, day, sequence) {
|
|
4998
5419
|
super(message);
|
|
@@ -5035,7 +5456,7 @@ async function importLegacyChronicleJournal(journal, directory) {
|
|
|
5035
5456
|
let importedFamilies = 0;
|
|
5036
5457
|
for (const familyBase of families) {
|
|
5037
5458
|
const day = dayOfFamily(familyBase);
|
|
5038
|
-
const basePath =
|
|
5459
|
+
const basePath = path9.join(directory, `${familyBase}.jsonl`);
|
|
5039
5460
|
let familyEvents = 0;
|
|
5040
5461
|
if (journal.hasImportedDay(day)) continue;
|
|
5041
5462
|
try {
|
|
@@ -5098,9 +5519,10 @@ async function importLegacyChronicleJournal(journal, directory) {
|
|
|
5098
5519
|
}
|
|
5099
5520
|
|
|
5100
5521
|
// src/chronicle/sqlite-journal.ts
|
|
5101
|
-
import { randomUUID as
|
|
5522
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
5523
|
+
import * as fs8 from "node:fs";
|
|
5102
5524
|
import { createRequire as createRequire2 } from "node:module";
|
|
5103
|
-
import * as
|
|
5525
|
+
import * as path11 from "node:path";
|
|
5104
5526
|
|
|
5105
5527
|
// src/utils/index.ts
|
|
5106
5528
|
init_atomic_write();
|
|
@@ -5129,7 +5551,7 @@ function envFlag(value) {
|
|
|
5129
5551
|
return !/^(0|false|no|off)$/i.test(value.trim());
|
|
5130
5552
|
}
|
|
5131
5553
|
var COLOR = isColorTty();
|
|
5132
|
-
var wrap = (
|
|
5554
|
+
var wrap = (open5, close) => (s) => COLOR ? `\x1B[${open5}m${s}\x1B[${close}m` : s;
|
|
5133
5555
|
var color = {
|
|
5134
5556
|
reset: wrap("0", "0"),
|
|
5135
5557
|
bold: wrap("1", "22"),
|
|
@@ -5149,6 +5571,20 @@ var color = {
|
|
|
5149
5571
|
bgGreen: wrap("42", "49")
|
|
5150
5572
|
};
|
|
5151
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
|
+
|
|
5152
5588
|
// src/skills/limits.ts
|
|
5153
5589
|
var SKILL_LIMITS = {
|
|
5154
5590
|
/**
|
|
@@ -5249,7 +5685,14 @@ var SKILL_LIMITS = {
|
|
|
5249
5685
|
};
|
|
5250
5686
|
|
|
5251
5687
|
// src/skills/frontmatter.ts
|
|
5252
|
-
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
|
+
]);
|
|
5253
5696
|
function parseSkillFrontmatter(raw) {
|
|
5254
5697
|
const text = normalizeLineEndings(raw);
|
|
5255
5698
|
if (!text.startsWith("---")) return {};
|
|
@@ -5308,8 +5751,9 @@ function parseFrontmatterBlock(block) {
|
|
|
5308
5751
|
out[normalizeKey2(key)] = collected.join("\n").trim();
|
|
5309
5752
|
continue;
|
|
5310
5753
|
}
|
|
5311
|
-
if (key === "allowed-tools" || key === "allowedTools") {
|
|
5312
|
-
|
|
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;
|
|
5313
5757
|
i++;
|
|
5314
5758
|
continue;
|
|
5315
5759
|
}
|
|
@@ -5323,7 +5767,11 @@ function parseFrontmatterBlock(block) {
|
|
|
5323
5767
|
return out;
|
|
5324
5768
|
}
|
|
5325
5769
|
function normalizeKey2(key) {
|
|
5326
|
-
|
|
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;
|
|
5327
5775
|
}
|
|
5328
5776
|
function unquote(s) {
|
|
5329
5777
|
if (s.length >= 2 && (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'"))) {
|
|
@@ -5364,7 +5812,7 @@ function capSkillBody(body) {
|
|
|
5364
5812
|
|
|
5365
5813
|
// src/utils/instruction-file.ts
|
|
5366
5814
|
import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
5367
|
-
import * as
|
|
5815
|
+
import * as path10 from "node:path";
|
|
5368
5816
|
import { fileURLToPath } from "node:url";
|
|
5369
5817
|
var textCache = /* @__PURE__ */ new Map();
|
|
5370
5818
|
var rootCandidates;
|
|
@@ -5374,7 +5822,7 @@ function readBundledInstructionText(relativePath) {
|
|
|
5374
5822
|
let resolved = "";
|
|
5375
5823
|
for (const root of instructionRootCandidates()) {
|
|
5376
5824
|
try {
|
|
5377
|
-
resolved = readFileSync2(
|
|
5825
|
+
resolved = readFileSync2(path10.join(root, relativePath), "utf8").trimEnd();
|
|
5378
5826
|
break;
|
|
5379
5827
|
} catch {
|
|
5380
5828
|
}
|
|
@@ -5390,11 +5838,11 @@ function renderInstructionTemplate(template, values) {
|
|
|
5390
5838
|
}
|
|
5391
5839
|
function instructionRootCandidates() {
|
|
5392
5840
|
if (rootCandidates !== void 0) return rootCandidates;
|
|
5393
|
-
const here =
|
|
5841
|
+
const here = path10.dirname(fileURLToPath(import.meta.url));
|
|
5394
5842
|
const candidates = [
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5843
|
+
path10.resolve(here, "../../instructions"),
|
|
5844
|
+
path10.resolve(here, "../instructions"),
|
|
5845
|
+
path10.resolve(here, "instructions")
|
|
5398
5846
|
];
|
|
5399
5847
|
rootCandidates = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
|
|
5400
5848
|
return rootCandidates;
|
|
@@ -5508,7 +5956,7 @@ var ChronicleSqliteQueryEngine = class {
|
|
|
5508
5956
|
const comparison = order === "asc" ? ">" : "<";
|
|
5509
5957
|
const keyset = cursor ? ` AND (day, sequence) ${comparison} (?, ?)` : "";
|
|
5510
5958
|
const sql = `SELECT day, sequence, payload FROM events WHERE ${pushed.clause}${keyset} ORDER BY day ${direction}, sequence ${direction} LIMIT ? OFFSET ?`;
|
|
5511
|
-
const
|
|
5959
|
+
const summary2 = createSummaryAccumulator();
|
|
5512
5960
|
const page = [];
|
|
5513
5961
|
let total = 0;
|
|
5514
5962
|
let scannedEvents = 0;
|
|
@@ -5532,7 +5980,7 @@ var ChronicleSqliteQueryEngine = class {
|
|
|
5532
5980
|
}
|
|
5533
5981
|
if (!matches(event, query)) continue;
|
|
5534
5982
|
total++;
|
|
5535
|
-
updateSummary(
|
|
5983
|
+
updateSummary(summary2, event);
|
|
5536
5984
|
if (page.length < limit) {
|
|
5537
5985
|
page.push(event);
|
|
5538
5986
|
last = { day: row.day, sequence: row.sequence };
|
|
@@ -5547,7 +5995,7 @@ var ChronicleSqliteQueryEngine = class {
|
|
|
5547
5995
|
scannedEvents,
|
|
5548
5996
|
sourceFiles: this.diagnostics.sourceFiles,
|
|
5549
5997
|
invalidLines: this.diagnostics.invalidLines,
|
|
5550
|
-
summary: finalizeSummary(
|
|
5998
|
+
summary: finalizeSummary(summary2)
|
|
5551
5999
|
};
|
|
5552
6000
|
if (total > page.length && last) {
|
|
5553
6001
|
return { ...result, nextCursor: encodeCursor2(last) };
|
|
@@ -5682,6 +6130,25 @@ var CHRONICLE_SQLITE_FILE = "chronicle.sqlite";
|
|
|
5682
6130
|
var LEGACY_JSONL_MIGRATION_KEY = "legacy-jsonl-v1";
|
|
5683
6131
|
var LEGACY_JSONL_QUARANTINE_KEY = "legacy-jsonl-v1:quarantine";
|
|
5684
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
|
+
};
|
|
5685
6152
|
var Ctor2;
|
|
5686
6153
|
function loadDatabaseSync2() {
|
|
5687
6154
|
if (Ctor2) return Ctor2;
|
|
@@ -5704,6 +6171,14 @@ var ChronicleSqliteJournal = class {
|
|
|
5704
6171
|
now;
|
|
5705
6172
|
monotonicNow;
|
|
5706
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;
|
|
5707
6182
|
/**
|
|
5708
6183
|
* Cached chain head per day. Cleared wholesale on any write failure so the
|
|
5709
6184
|
* next append rebuilds from the database rather than trusting a counter that
|
|
@@ -5721,14 +6196,40 @@ var ChronicleSqliteJournal = class {
|
|
|
5721
6196
|
};
|
|
5722
6197
|
lastBatchDurationMs;
|
|
5723
6198
|
constructor(options) {
|
|
5724
|
-
this.dbPath =
|
|
6199
|
+
this.dbPath = path11.join(path11.resolve(options.directory), CHRONICLE_SQLITE_FILE);
|
|
5725
6200
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
5726
6201
|
this.monotonicNow = options.monotonicNow ?? (() => process.hrtime.bigint());
|
|
5727
|
-
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;
|
|
5728
6223
|
const Database = loadDatabaseSync2();
|
|
5729
6224
|
this.db = new Database(this.dbPath);
|
|
5730
|
-
this.db.exec(
|
|
6225
|
+
this.db.exec(`PRAGMA journal_mode = ${this.maxBytes === void 0 ? "WAL" : "DELETE"}`);
|
|
5731
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
|
+
}
|
|
5732
6233
|
}
|
|
5733
6234
|
close() {
|
|
5734
6235
|
this.db.close();
|
|
@@ -5787,8 +6288,10 @@ var ChronicleSqliteJournal = class {
|
|
|
5787
6288
|
events.push(event);
|
|
5788
6289
|
previous = { sequence: event.sequence, hash: event.hash };
|
|
5789
6290
|
}
|
|
6291
|
+
let retainedCountAfterCommit = this.retainedEventCount;
|
|
5790
6292
|
try {
|
|
5791
6293
|
this.db.exec("BEGIN IMMEDIATE");
|
|
6294
|
+
this.assertWithinByteQuota(events);
|
|
5792
6295
|
const insert = this.db.prepare(
|
|
5793
6296
|
`INSERT INTO events (
|
|
5794
6297
|
day, sequence, event_id, hash, previous_hash, occurred_at, event_type, outcome,
|
|
@@ -5820,6 +6323,8 @@ var ChronicleSqliteJournal = class {
|
|
|
5820
6323
|
JSON.stringify(event)
|
|
5821
6324
|
);
|
|
5822
6325
|
}
|
|
6326
|
+
retainedCountAfterCommit = this.enforceEventLimitWithinTransaction();
|
|
6327
|
+
this.assertActualAllocationWithinQuota();
|
|
5823
6328
|
this.db.exec("COMMIT");
|
|
5824
6329
|
} catch (error) {
|
|
5825
6330
|
try {
|
|
@@ -5829,11 +6334,13 @@ var ChronicleSqliteJournal = class {
|
|
|
5829
6334
|
this.anchors.clear();
|
|
5830
6335
|
this.counters.failedEvents += inputs.length;
|
|
5831
6336
|
this.lastBatchDurationMs = performance.now() - started;
|
|
5832
|
-
throw error;
|
|
6337
|
+
throw this.normalizeQuotaError(error);
|
|
5833
6338
|
}
|
|
5834
6339
|
this.anchors.set(day, previous);
|
|
5835
6340
|
this.counters.persistedEvents += events.length;
|
|
6341
|
+
this.retainedEventCount = retainedCountAfterCommit;
|
|
5836
6342
|
this.lastBatchDurationMs = performance.now() - started;
|
|
6343
|
+
await this.enforceRetentionIfDue();
|
|
5837
6344
|
return events;
|
|
5838
6345
|
}
|
|
5839
6346
|
async readAll() {
|
|
@@ -5894,6 +6401,145 @@ var ChronicleSqliteJournal = class {
|
|
|
5894
6401
|
}
|
|
5895
6402
|
return { ok: true, entries, lastSequence, lastHash };
|
|
5896
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
|
+
}
|
|
6430
|
+
/**
|
|
6431
|
+
* Bytes the journal actually occupies: live pages in the main database plus
|
|
6432
|
+
* whatever the rollback/WAL sidecars currently hold.
|
|
6433
|
+
*
|
|
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.
|
|
6447
|
+
*/
|
|
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
|
+
}
|
|
6456
|
+
}
|
|
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
|
+
}
|
|
5897
6543
|
/**
|
|
5898
6544
|
* Drop events older than the retention window.
|
|
5899
6545
|
*
|
|
@@ -5939,6 +6585,7 @@ var ChronicleSqliteJournal = class {
|
|
|
5939
6585
|
};
|
|
5940
6586
|
}
|
|
5941
6587
|
this.anchors.clear();
|
|
6588
|
+
this.retainedEventCount = Math.max(0, this.retainedEventCount - count);
|
|
5942
6589
|
return { ...empty, deletedCount: count };
|
|
5943
6590
|
}
|
|
5944
6591
|
/**
|
|
@@ -5970,7 +6617,9 @@ var ChronicleSqliteJournal = class {
|
|
|
5970
6617
|
ON CONFLICT(day) DO UPDATE SET sequence = excluded.sequence, hash = excluded.hash`
|
|
5971
6618
|
);
|
|
5972
6619
|
this.db.exec("BEGIN IMMEDIATE");
|
|
6620
|
+
let retainedCountAfterCommit = this.retainedEventCount;
|
|
5973
6621
|
try {
|
|
6622
|
+
this.assertWithinByteQuota([]);
|
|
5974
6623
|
await load({
|
|
5975
6624
|
insert: (day, event) => {
|
|
5976
6625
|
const row = projectEvent(event);
|
|
@@ -6000,16 +6649,19 @@ var ChronicleSqliteJournal = class {
|
|
|
6000
6649
|
checkpoint.run(day, sequence, hash);
|
|
6001
6650
|
}
|
|
6002
6651
|
});
|
|
6652
|
+
retainedCountAfterCommit = this.enforceEventLimitWithinTransaction();
|
|
6653
|
+
this.assertActualAllocationWithinQuota();
|
|
6003
6654
|
this.db.exec("COMMIT");
|
|
6004
6655
|
} catch (error) {
|
|
6005
6656
|
try {
|
|
6006
6657
|
this.db.exec("ROLLBACK");
|
|
6007
6658
|
} catch {
|
|
6008
6659
|
}
|
|
6009
|
-
throw error;
|
|
6660
|
+
throw this.normalizeQuotaError(error);
|
|
6010
6661
|
} finally {
|
|
6011
6662
|
this.anchors.clear();
|
|
6012
6663
|
}
|
|
6664
|
+
this.retainedEventCount = retainedCountAfterCommit;
|
|
6013
6665
|
}
|
|
6014
6666
|
/**
|
|
6015
6667
|
* A read engine over this journal's own connection.
|
|
@@ -6165,15 +6817,15 @@ function projectEvent(event) {
|
|
|
6165
6817
|
|
|
6166
6818
|
// src/chronicle/project-server-client.ts
|
|
6167
6819
|
import { spawn } from "node:child_process";
|
|
6168
|
-
import * as
|
|
6820
|
+
import * as fs9 from "node:fs";
|
|
6169
6821
|
import * as net from "node:net";
|
|
6170
|
-
import * as
|
|
6822
|
+
import * as path13 from "node:path";
|
|
6171
6823
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6172
6824
|
|
|
6173
6825
|
// src/chronicle/project-server-endpoint.ts
|
|
6174
6826
|
import { createHash as createHash6 } from "node:crypto";
|
|
6175
6827
|
import * as os3 from "node:os";
|
|
6176
|
-
import * as
|
|
6828
|
+
import * as path12 from "node:path";
|
|
6177
6829
|
|
|
6178
6830
|
// src/chronicle/project-server-protocol.ts
|
|
6179
6831
|
var CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION = 2;
|
|
@@ -6186,39 +6838,39 @@ function encodeChronicleProjectServerMessage(message) {
|
|
|
6186
6838
|
// src/chronicle/project-server-endpoint.ts
|
|
6187
6839
|
var CHRONICLE_PROJECT_SERVER_METADATA_FILE = "server.json";
|
|
6188
6840
|
function normalizedPath(value) {
|
|
6189
|
-
const resolved =
|
|
6841
|
+
const resolved = path12.resolve(value);
|
|
6190
6842
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
6191
6843
|
}
|
|
6192
6844
|
function chronicleProjectServerKey(projectDir) {
|
|
6193
|
-
return createHash6("sha256").update(normalizedPath(
|
|
6845
|
+
return createHash6("sha256").update(normalizedPath(path12.join(projectDir, "chronicle"))).digest("hex").slice(0, 24);
|
|
6194
6846
|
}
|
|
6195
6847
|
function chronicleProjectServerEndpoint(projectDir) {
|
|
6196
6848
|
const key = chronicleProjectServerKey(projectDir);
|
|
6197
6849
|
if (process.platform === "win32") {
|
|
6198
6850
|
return `\\\\.\\pipe\\wrongstack-chronicle-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
|
|
6199
6851
|
}
|
|
6200
|
-
return
|
|
6852
|
+
return path12.join(
|
|
6201
6853
|
os3.tmpdir(),
|
|
6202
6854
|
`wsch-v${CHRONICLE_PROJECT_SERVER_PROTOCOL_VERSION}`,
|
|
6203
6855
|
`${key}.sock`
|
|
6204
6856
|
);
|
|
6205
6857
|
}
|
|
6206
6858
|
function chronicleProjectServerMetadataPath(projectDir) {
|
|
6207
|
-
return
|
|
6859
|
+
return path12.join(projectDir, "chronicle", CHRONICLE_PROJECT_SERVER_METADATA_FILE);
|
|
6208
6860
|
}
|
|
6209
6861
|
|
|
6210
6862
|
// src/chronicle/project-server-client.ts
|
|
6211
6863
|
var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
|
|
6212
6864
|
var SERVER_START_TIMEOUT_MS = 1e4;
|
|
6213
6865
|
var DEFAULT_CALL_TIMEOUT_MS = 3e4;
|
|
6214
|
-
function resolveChronicleDaemonAvailability(moduleUrl = import.meta.url, exists =
|
|
6866
|
+
function resolveChronicleDaemonAvailability(moduleUrl = import.meta.url, exists = fs9.existsSync) {
|
|
6215
6867
|
if (process.env["WRONGSTACK_CHRONICLE_INLINE"] || process.env["WRONGSTACK_CHRONICLE_SERVER"] === "0") {
|
|
6216
6868
|
return { kind: "inline-requested" };
|
|
6217
6869
|
}
|
|
6218
6870
|
const url = locateChronicleProjectServer(moduleUrl, exists);
|
|
6219
6871
|
return url ? { kind: "available", url } : { kind: "missing-build" };
|
|
6220
6872
|
}
|
|
6221
|
-
function resolveChronicleProjectServerUrl(moduleUrl = import.meta.url, exists =
|
|
6873
|
+
function resolveChronicleProjectServerUrl(moduleUrl = import.meta.url, exists = fs9.existsSync) {
|
|
6222
6874
|
const availability = resolveChronicleDaemonAvailability(moduleUrl, exists);
|
|
6223
6875
|
return availability.kind === "available" ? availability.url : null;
|
|
6224
6876
|
}
|
|
@@ -6387,7 +7039,7 @@ var ChronicleProjectServerClient = class {
|
|
|
6387
7039
|
currentAuthToken() {
|
|
6388
7040
|
if (this.authToken === void 0) {
|
|
6389
7041
|
try {
|
|
6390
|
-
const raw =
|
|
7042
|
+
const raw = fs9.readFileSync(
|
|
6391
7043
|
chronicleProjectServerMetadataPath(this.options.projectDir),
|
|
6392
7044
|
"utf8"
|
|
6393
7045
|
);
|
|
@@ -6525,7 +7177,7 @@ var ChronicleProjectServerClient = class {
|
|
|
6525
7177
|
}
|
|
6526
7178
|
};
|
|
6527
7179
|
function normalizePath(value) {
|
|
6528
|
-
const resolved =
|
|
7180
|
+
const resolved = path13.resolve(value);
|
|
6529
7181
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
6530
7182
|
}
|
|
6531
7183
|
|
|
@@ -6533,7 +7185,7 @@ function normalizePath(value) {
|
|
|
6533
7185
|
function resolveChronicleProjectServerOptions(options) {
|
|
6534
7186
|
if (options.projectPaths) {
|
|
6535
7187
|
return {
|
|
6536
|
-
projectRoot:
|
|
7188
|
+
projectRoot: path14.resolve(options.projectRoot),
|
|
6537
7189
|
...options.projectPaths,
|
|
6538
7190
|
...options.retentionDays !== void 0 ? { retentionDays: options.retentionDays } : {}
|
|
6539
7191
|
};
|
|
@@ -6543,7 +7195,7 @@ function resolveChronicleProjectServerOptions(options) {
|
|
|
6543
7195
|
userHome: options.userHome ?? os4.homedir()
|
|
6544
7196
|
});
|
|
6545
7197
|
return {
|
|
6546
|
-
projectRoot:
|
|
7198
|
+
projectRoot: path14.resolve(options.projectRoot),
|
|
6547
7199
|
globalRoot: paths.globalRoot,
|
|
6548
7200
|
projectId: paths.projectHash,
|
|
6549
7201
|
projectDir: paths.projectDir,
|
|
@@ -6571,7 +7223,7 @@ function assertInlineWasRequested(caller) {
|
|
|
6571
7223
|
);
|
|
6572
7224
|
}
|
|
6573
7225
|
function legacyPartitionPath(location) {
|
|
6574
|
-
return
|
|
7226
|
+
return path14.join(location.chronicleDirectory, `${location.day}.events.jsonl`);
|
|
6575
7227
|
}
|
|
6576
7228
|
function useSqliteStore() {
|
|
6577
7229
|
return process.env["WRONGSTACK_CHRONICLE_STORE"] !== "jsonl";
|
|
@@ -6677,7 +7329,7 @@ var InlineChronicleProjectAccess = class {
|
|
|
6677
7329
|
}
|
|
6678
7330
|
}
|
|
6679
7331
|
get chronicleDirectory() {
|
|
6680
|
-
return
|
|
7332
|
+
return path14.join(this.options.projectDir, "chronicle");
|
|
6681
7333
|
}
|
|
6682
7334
|
journalForToday() {
|
|
6683
7335
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -6886,16 +7538,16 @@ async function findActiveKanbanCard(projectRoot) {
|
|
|
6886
7538
|
}
|
|
6887
7539
|
async function findFileProvenance(projectRoot, filePaths) {
|
|
6888
7540
|
if (filePaths.length === 0) return [];
|
|
6889
|
-
let
|
|
7541
|
+
let access7;
|
|
6890
7542
|
try {
|
|
6891
|
-
|
|
7543
|
+
access7 = createChronicleProjectAccess({ projectRoot });
|
|
6892
7544
|
} catch {
|
|
6893
7545
|
return [];
|
|
6894
7546
|
}
|
|
6895
7547
|
const provenance = [];
|
|
6896
7548
|
try {
|
|
6897
7549
|
const uniquePaths = [...new Set(filePaths)];
|
|
6898
|
-
const result = await
|
|
7550
|
+
const result = await access7.call("metrics", {
|
|
6899
7551
|
view: "files",
|
|
6900
7552
|
refresh: false,
|
|
6901
7553
|
files: {
|
|
@@ -6920,7 +7572,7 @@ async function findFileProvenance(projectRoot, filePaths) {
|
|
|
6920
7572
|
});
|
|
6921
7573
|
}
|
|
6922
7574
|
} finally {
|
|
6923
|
-
await
|
|
7575
|
+
await access7.close();
|
|
6924
7576
|
}
|
|
6925
7577
|
return provenance;
|
|
6926
7578
|
}
|
|
@@ -7017,6 +7669,7 @@ function resolveAutoReviewConfig(cfg, sessionConfig) {
|
|
|
7017
7669
|
provider: resolvedProvider,
|
|
7018
7670
|
model: resolvedModel,
|
|
7019
7671
|
fallbackModels,
|
|
7672
|
+
modelSelection: cfg.modelSelection === "random" ? "random" : "round-robin",
|
|
7020
7673
|
debounceMs: cfg.debounceMs ?? DEFAULT_DEBOUNCE_MS,
|
|
7021
7674
|
maxFilesPerBatch: cfg.maxFilesPerBatch ?? DEFAULT_MAX_FILES_PER_BATCH,
|
|
7022
7675
|
maxConcurrentReviews: cfg.maxConcurrentReviews ?? DEFAULT_MAX_CONCURRENT_REVIEWS,
|
|
@@ -7137,9 +7790,9 @@ async function snapshotChangedFiles(cwd) {
|
|
|
7137
7790
|
if (file.path.startsWith(".wrongstack/")) continue;
|
|
7138
7791
|
if (budget <= 0) break;
|
|
7139
7792
|
try {
|
|
7140
|
-
const
|
|
7141
|
-
if (!
|
|
7142
|
-
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");
|
|
7143
7796
|
budget -= content.length;
|
|
7144
7797
|
snapshots.push({
|
|
7145
7798
|
...file,
|
|
@@ -7161,7 +7814,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7161
7814
|
"",
|
|
7162
7815
|
"Continuous code review that fires on every change during a session.",
|
|
7163
7816
|
"Detects git-tracked file edits and dispatches review subagents",
|
|
7164
|
-
"with configurable provider/model/fallback.",
|
|
7817
|
+
"with configurable provider/model/fallback and profile selection.",
|
|
7165
7818
|
"",
|
|
7166
7819
|
"Reports are persisted and shown as passive notifications.",
|
|
7167
7820
|
"They never wake the leader or spawn mutating follow-up agents.",
|
|
@@ -7176,6 +7829,7 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7176
7829
|
" provider provider id for review agents",
|
|
7177
7830
|
" model model id for review agents",
|
|
7178
7831
|
" fallbackProfile named profile from config.fallbackProfiles",
|
|
7832
|
+
" modelSelection round-robin | random (default round-robin)",
|
|
7179
7833
|
" debounceMs debounce window (default 15000)",
|
|
7180
7834
|
" maxFilesPerBatch max files per review (default 15)",
|
|
7181
7835
|
" maxConcurrentReviews max parallel reviews (default 2)",
|
|
@@ -7202,7 +7856,8 @@ function buildAutoReviewCommand(getConfig, getInFlightCount) {
|
|
|
7202
7856
|
"",
|
|
7203
7857
|
` Provider: ${cfg.provider}`,
|
|
7204
7858
|
` Model: ${cfg.model}`,
|
|
7205
|
-
` Fallback:
|
|
7859
|
+
` Fallback / RR: ${cfg.fallbackModels.length > 0 ? cfg.fallbackModels.join(", ") : "(none)"}`,
|
|
7860
|
+
` Selection: ${cfg.modelSelection}`,
|
|
7206
7861
|
` Debounce: ${cfg.debounceMs} ms`,
|
|
7207
7862
|
` Max files: ${cfg.maxFilesPerBatch}`,
|
|
7208
7863
|
` Max parallel: ${cfg.maxConcurrentReviews}`,
|
|
@@ -7421,6 +8076,7 @@ function createAutoReviewPlugin() {
|
|
|
7421
8076
|
maxCascadeDepth: cfg.maxCascadeDepth
|
|
7422
8077
|
});
|
|
7423
8078
|
bundle.reviewFallbackModels = [...cfg.fallbackModels];
|
|
8079
|
+
bundle.reviewModelSelection = cfg.modelSelection;
|
|
7424
8080
|
const trackedChangedPaths = new Set(
|
|
7425
8081
|
snapshots.filter((file) => !file.path.startsWith(".wrongstack/")).map((file) => file.path)
|
|
7426
8082
|
);
|
|
@@ -7431,7 +8087,7 @@ function createAutoReviewPlugin() {
|
|
|
7431
8087
|
api.log.info(
|
|
7432
8088
|
`[auto-review] #${reviewCounter} emitting review_needed (${filesWithContent.length} files, provider=${cfg.provider} model=${cfg.model})`
|
|
7433
8089
|
);
|
|
7434
|
-
const emittedBundle = emitReviewIfChanged(api, bundle);
|
|
8090
|
+
const emittedBundle = await emitReviewIfChanged(api, bundle);
|
|
7435
8091
|
if (!emittedBundle) {
|
|
7436
8092
|
api.log.info(
|
|
7437
8093
|
`[auto-review] #${reviewCounter} skipped \u2014 file content already has a review in progress`
|
|
@@ -7464,89 +8120,94 @@ function createAutoReviewPlugin() {
|
|
|
7464
8120
|
api.onEvent("iteration.completed", async (payload) => {
|
|
7465
8121
|
await handleIterationCompleted?.(payload);
|
|
7466
8122
|
});
|
|
7467
|
-
api.onEvent("session.ended",
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
`[auto-review] session ended \u2014 handed ${handedOffFiles} pending mid-session file(s) to post-session review`
|
|
7474
|
-
);
|
|
7475
|
-
}
|
|
7476
|
-
await pendingReviewWork;
|
|
7477
|
-
try {
|
|
7478
|
-
const cfg = resolved;
|
|
7479
|
-
if (!cfg.enabled) return;
|
|
7480
|
-
const cwd = api.config.cwd ?? process.cwd();
|
|
7481
|
-
if (!await isGitRepo(cwd)) return;
|
|
7482
|
-
const allChanged = await getChangedFiles(cwd);
|
|
7483
|
-
const existing = [];
|
|
7484
|
-
for (const f of allChanged) {
|
|
7485
|
-
if (f.path.startsWith(".wrongstack/")) continue;
|
|
7486
|
-
try {
|
|
7487
|
-
await fsp.access(path14.join(cwd, f.path));
|
|
7488
|
-
existing.push(f);
|
|
7489
|
-
} catch {
|
|
7490
|
-
}
|
|
7491
|
-
}
|
|
7492
|
-
if (existing.length === 0) return;
|
|
7493
|
-
const toReview = existing.slice(0, cfg.maxFilesPerBatch);
|
|
7494
|
-
const filesWithContent = [];
|
|
7495
|
-
for (const f of toReview) {
|
|
7496
|
-
try {
|
|
7497
|
-
const absPath = path14.join(cwd, f.path);
|
|
7498
|
-
const content = await fsp.readFile(absPath, "utf8");
|
|
7499
|
-
filesWithContent.push({ path: f.path, status: f.status, content });
|
|
7500
|
-
} catch {
|
|
7501
|
-
}
|
|
7502
|
-
}
|
|
7503
|
-
if (filesWithContent.length === 0) return;
|
|
7504
|
-
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) {
|
|
7505
8129
|
api.log.info(
|
|
7506
|
-
`[auto-review] session
|
|
8130
|
+
`[auto-review] session ended \u2014 handed ${handedOffFiles} pending mid-session file(s) to post-session review`
|
|
7507
8131
|
);
|
|
7508
|
-
return;
|
|
7509
8132
|
}
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
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);
|
|
7532
8202
|
api.log.info(
|
|
7533
|
-
|
|
8203
|
+
`[auto-review] session end \u2014 final review (${emittedBundle.files.length} files)`
|
|
7534
8204
|
);
|
|
7535
|
-
|
|
8205
|
+
} catch (err) {
|
|
8206
|
+
api.log.warn(`[auto-review] session.ended handler failed: ${toErrorMessage(err)}`);
|
|
7536
8207
|
}
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
subagentType: "review"
|
|
7541
|
-
};
|
|
7542
|
-
inFlight.push(inflightEntry);
|
|
7543
|
-
expireInFlight(inflightEntry);
|
|
7544
|
-
api.log.info(
|
|
7545
|
-
`[auto-review] session end \u2014 final review (${emittedBundle.files.length} files)`
|
|
7546
|
-
);
|
|
7547
|
-
} catch (err) {
|
|
7548
|
-
api.log.warn(`[auto-review] session.ended handler failed: ${toErrorMessage(err)}`);
|
|
7549
|
-
}
|
|
8208
|
+
})();
|
|
8209
|
+
event?.waitUntil?.(work);
|
|
8210
|
+
return work;
|
|
7550
8211
|
});
|
|
7551
8212
|
api.onPattern("chimera.review_complete", (_event, payload) => {
|
|
7552
8213
|
try {
|
|
@@ -7592,16 +8253,16 @@ function createAutoReviewPlugin() {
|
|
|
7592
8253
|
// src/plugins/chimera-plugin.ts
|
|
7593
8254
|
init_error();
|
|
7594
8255
|
import { spawn as spawn4 } from "node:child_process";
|
|
7595
|
-
import * as
|
|
8256
|
+
import * as fsp5 from "node:fs/promises";
|
|
7596
8257
|
import * as os5 from "node:os";
|
|
7597
|
-
import * as
|
|
8258
|
+
import * as path18 from "node:path";
|
|
7598
8259
|
|
|
7599
8260
|
// src/plugins/review-finding-commands.ts
|
|
7600
8261
|
init_review_finding_store();
|
|
7601
8262
|
|
|
7602
8263
|
// src/plugins/review-finding-parser.ts
|
|
7603
8264
|
init_review_finding_types();
|
|
7604
|
-
import { randomUUID as
|
|
8265
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
7605
8266
|
var SUGGEST_LINE = /^\s*(?:→|->|=>)\s*(.+)$/;
|
|
7606
8267
|
var DURATION_LINE = /^Duration:\s*(\d+)s\s*$/im;
|
|
7607
8268
|
function parseChimeraReviewReport(reportText, context = {}) {
|
|
@@ -7609,7 +8270,7 @@ function parseChimeraReviewReport(reportText, context = {}) {
|
|
|
7609
8270
|
return { findings: [], unparseableCount: 0 };
|
|
7610
8271
|
}
|
|
7611
8272
|
const findings = [];
|
|
7612
|
-
const reportId = context.reportId ??
|
|
8273
|
+
const reportId = context.reportId ?? randomUUID5();
|
|
7613
8274
|
let unparseableCount = 0;
|
|
7614
8275
|
let durationSeconds;
|
|
7615
8276
|
let currentSeverity = null;
|
|
@@ -7710,7 +8371,7 @@ function parseFindingSegment(segment, severity, context) {
|
|
|
7710
8371
|
const fullDesc = suggestions.length > 0 ? cleanDesc + "\n" + suggestions.map((s) => " \u2192 " + s).join("\n") : cleanDesc;
|
|
7711
8372
|
const suggestedFix = suggestions.length > 0 ? suggestions.join("\n") : void 0;
|
|
7712
8373
|
return {
|
|
7713
|
-
id:
|
|
8374
|
+
id: randomUUID5(),
|
|
7714
8375
|
fingerprint: computeFindingFingerprint(file ?? "", line ?? null, title),
|
|
7715
8376
|
severity,
|
|
7716
8377
|
source: normalizeFindingSource(context.reviewType),
|
|
@@ -7721,7 +8382,7 @@ function parseFindingSegment(segment, severity, context) {
|
|
|
7721
8382
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7722
8383
|
status: "active",
|
|
7723
8384
|
originReport: {
|
|
7724
|
-
reportId: context.reportId ??
|
|
8385
|
+
reportId: context.reportId ?? randomUUID5(),
|
|
7725
8386
|
sessionId: context.sessionId ?? "",
|
|
7726
8387
|
agentId: context.agentId ?? "",
|
|
7727
8388
|
reviewerModel: context.reviewerModel ?? ""
|
|
@@ -8375,7 +9036,10 @@ function buildChimeraCommand(getConfig, events) {
|
|
|
8375
9036
|
].join("\n")
|
|
8376
9037
|
};
|
|
8377
9038
|
}
|
|
8378
|
-
events.emit(
|
|
9039
|
+
events.emit(
|
|
9040
|
+
"chimera.set_autofix",
|
|
9041
|
+
{ mode }
|
|
9042
|
+
);
|
|
8379
9043
|
return {
|
|
8380
9044
|
message: `\u2705 Chimera autoFix set to "${mode}" for this session (config file unchanged).`
|
|
8381
9045
|
};
|
|
@@ -8414,8 +9078,8 @@ function buildReviewCommand(_getConfig) {
|
|
|
8414
9078
|
async run(args) {
|
|
8415
9079
|
const cwd = process.cwd();
|
|
8416
9080
|
const slug = projectSlug(cwd);
|
|
8417
|
-
const globalRoot =
|
|
8418
|
-
const projectDir =
|
|
9081
|
+
const globalRoot = path18.join(os5.homedir(), ".wrongstack");
|
|
9082
|
+
const projectDir = path18.join(globalRoot, "projects", slug);
|
|
8419
9083
|
const trimmed = (args ?? "").trim();
|
|
8420
9084
|
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
8421
9085
|
return {
|
|
@@ -8448,10 +9112,10 @@ function createChimeraPlugin() {
|
|
|
8448
9112
|
}
|
|
8449
9113
|
});
|
|
8450
9114
|
api.onPattern("chimera.review_needed", (_event, payload) => {
|
|
8451
|
-
recordStartedReview(api.events, payload);
|
|
9115
|
+
void recordStartedReview(api.events, payload).catch(() => void 0);
|
|
8452
9116
|
});
|
|
8453
9117
|
api.onPattern("chimera.review_complete", (_event, payload) => {
|
|
8454
|
-
recordCompletedReview(api.events, payload);
|
|
9118
|
+
void recordCompletedReview(api.events, payload).catch(() => void 0);
|
|
8455
9119
|
});
|
|
8456
9120
|
if (!resolved.enabled) {
|
|
8457
9121
|
api.log.info("[chimera] disabled by config");
|
|
@@ -8462,64 +9126,70 @@ function createChimeraPlugin() {
|
|
|
8462
9126
|
);
|
|
8463
9127
|
api.slashCommands.register(buildChimeraCommand(() => resolved, api.events));
|
|
8464
9128
|
api.slashCommands.register(buildReviewCommand(() => resolved));
|
|
8465
|
-
api.onEvent("session.ended",
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
const allChanged = await getChangedFiles2(cwd);
|
|
8475
|
-
const existing = [];
|
|
8476
|
-
for (const f of allChanged) {
|
|
8477
|
-
if (f.path.startsWith(".wrongstack/")) continue;
|
|
8478
|
-
try {
|
|
8479
|
-
await fsp4.access(path17.join(cwd, f.path));
|
|
8480
|
-
existing.push(f);
|
|
8481
|
-
} 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;
|
|
8482
9138
|
}
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
const filesWithContent = [];
|
|
8493
|
-
for (const f of toReview) {
|
|
8494
|
-
try {
|
|
8495
|
-
const absPath = path17.join(cwd, f.path);
|
|
8496
|
-
const content = await fsp4.readFile(absPath, "utf8");
|
|
8497
|
-
filesWithContent.push({ path: f.path, status: f.status, content });
|
|
8498
|
-
} 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
|
+
}
|
|
8499
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)}`);
|
|
8500
9189
|
}
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
}
|
|
8505
|
-
const bundle = await buildReviewContext({
|
|
8506
|
-
cwd,
|
|
8507
|
-
config: cfg,
|
|
8508
|
-
files: filesWithContent,
|
|
8509
|
-
cascadeOn: cfg.cascadeOn,
|
|
8510
|
-
maxCascadeDepth: cfg.maxCascadeDepth
|
|
8511
|
-
});
|
|
8512
|
-
const emittedBundle = emitReviewIfChanged(api, bundle);
|
|
8513
|
-
if (!emittedBundle) {
|
|
8514
|
-
api.log.info("[chimera] skipped \u2014 changed files already have reviews in progress");
|
|
8515
|
-
return;
|
|
8516
|
-
}
|
|
8517
|
-
api.log.info(
|
|
8518
|
-
`[chimera] emitted review_needed event (${emittedBundle.files.length} files)`
|
|
8519
|
-
);
|
|
8520
|
-
} catch (err) {
|
|
8521
|
-
api.log.warn(`[chimera] session.ended handler failed: ${toErrorMessage(err)}`);
|
|
8522
|
-
}
|
|
9190
|
+
})();
|
|
9191
|
+
event?.waitUntil?.(work);
|
|
9192
|
+
return work;
|
|
8523
9193
|
});
|
|
8524
9194
|
},
|
|
8525
9195
|
teardown(api) {
|
|
@@ -8534,17 +9204,17 @@ function createChimeraPlugin() {
|
|
|
8534
9204
|
}
|
|
8535
9205
|
|
|
8536
9206
|
// src/plugins/prompts-plugin.ts
|
|
8537
|
-
import * as
|
|
8538
|
-
import * as
|
|
9207
|
+
import * as fs13 from "node:fs/promises";
|
|
9208
|
+
import * as path21 from "node:path";
|
|
8539
9209
|
|
|
8540
9210
|
// src/execution/prompt-loader.ts
|
|
8541
|
-
import * as
|
|
8542
|
-
import * as
|
|
9211
|
+
import * as fs11 from "node:fs/promises";
|
|
9212
|
+
import * as path20 from "node:path";
|
|
8543
9213
|
|
|
8544
9214
|
// src/storage/prompt-store.ts
|
|
8545
9215
|
init_atomic_write();
|
|
8546
|
-
import * as
|
|
8547
|
-
import * as
|
|
9216
|
+
import * as fs10 from "node:fs/promises";
|
|
9217
|
+
import * as path19 from "node:path";
|
|
8548
9218
|
var SCHEMA_VERSION3 = 2;
|
|
8549
9219
|
function migratePromptEntry(raw) {
|
|
8550
9220
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -8605,12 +9275,12 @@ var DefaultPromptStore = class {
|
|
|
8605
9275
|
await ensureDir(this.dir);
|
|
8606
9276
|
const entries = [];
|
|
8607
9277
|
try {
|
|
8608
|
-
const files = await
|
|
9278
|
+
const files = await fs10.readdir(this.dir);
|
|
8609
9279
|
for (const file of files) {
|
|
8610
9280
|
if (!file.endsWith(".json")) continue;
|
|
8611
9281
|
try {
|
|
8612
9282
|
const raw = JSON.parse(
|
|
8613
|
-
await
|
|
9283
|
+
await fs10.readFile(path19.join(this.dir, file), "utf8")
|
|
8614
9284
|
);
|
|
8615
9285
|
const migrated = migratePromptEntry(raw.entry);
|
|
8616
9286
|
if (migrated) entries.push(migrated);
|
|
@@ -8624,9 +9294,9 @@ var DefaultPromptStore = class {
|
|
|
8624
9294
|
);
|
|
8625
9295
|
}
|
|
8626
9296
|
async get(id) {
|
|
8627
|
-
const file =
|
|
9297
|
+
const file = path19.join(this.dir, `${id}.json`);
|
|
8628
9298
|
try {
|
|
8629
|
-
const raw = JSON.parse(await
|
|
9299
|
+
const raw = JSON.parse(await fs10.readFile(file, "utf8"));
|
|
8630
9300
|
return migratePromptEntry(raw.entry);
|
|
8631
9301
|
} catch {
|
|
8632
9302
|
return null;
|
|
@@ -8634,14 +9304,14 @@ var DefaultPromptStore = class {
|
|
|
8634
9304
|
}
|
|
8635
9305
|
async save(entry) {
|
|
8636
9306
|
await ensureDir(this.dir);
|
|
8637
|
-
const file =
|
|
9307
|
+
const file = path19.join(this.dir, `${entry.id}.json`);
|
|
8638
9308
|
const raw = { version: SCHEMA_VERSION3, entry };
|
|
8639
9309
|
await atomicWrite(file, JSON.stringify(raw, null, 2));
|
|
8640
9310
|
}
|
|
8641
9311
|
async delete(id) {
|
|
8642
|
-
const file =
|
|
9312
|
+
const file = path19.join(this.dir, `${id}.json`);
|
|
8643
9313
|
try {
|
|
8644
|
-
await
|
|
9314
|
+
await fs10.unlink(file);
|
|
8645
9315
|
return true;
|
|
8646
9316
|
} catch {
|
|
8647
9317
|
return false;
|
|
@@ -8728,7 +9398,7 @@ var DefaultPromptLoader = class {
|
|
|
8728
9398
|
constructor(opts) {
|
|
8729
9399
|
this.projectStore = typeof opts.paths.inProjectPrompts === "string" ? new DefaultPromptStore(opts.paths.inProjectPrompts) : void 0;
|
|
8730
9400
|
this.userStore = typeof opts.paths.globalPrompts === "string" ? new DefaultPromptStore(opts.paths.globalPrompts) : void 0;
|
|
8731
|
-
this.builtinDir = opts.bundledDir ?
|
|
9401
|
+
this.builtinDir = opts.bundledDir ? path20.join(opts.bundledDir, "prompts") : void 0;
|
|
8732
9402
|
}
|
|
8733
9403
|
async list() {
|
|
8734
9404
|
if (this.cache) return this.cache;
|
|
@@ -8841,7 +9511,7 @@ var DefaultPromptLoader = class {
|
|
|
8841
9511
|
const files = await walkJson(dir);
|
|
8842
9512
|
for (const file of files) {
|
|
8843
9513
|
try {
|
|
8844
|
-
const parsed = JSON.parse(await
|
|
9514
|
+
const parsed = JSON.parse(await fs11.readFile(file, "utf8"));
|
|
8845
9515
|
const migrated = migratePromptEntry(parsed);
|
|
8846
9516
|
if (migrated) out.push({ ...migrated, source: "builtin" });
|
|
8847
9517
|
} catch {
|
|
@@ -8855,12 +9525,12 @@ async function walkJson(dir) {
|
|
|
8855
9525
|
const out = [];
|
|
8856
9526
|
let entries;
|
|
8857
9527
|
try {
|
|
8858
|
-
entries = await
|
|
9528
|
+
entries = await fs11.readdir(dir, { withFileTypes: true });
|
|
8859
9529
|
} catch {
|
|
8860
9530
|
return out;
|
|
8861
9531
|
}
|
|
8862
9532
|
for (const e of entries) {
|
|
8863
|
-
const full =
|
|
9533
|
+
const full = path20.join(dir, e.name);
|
|
8864
9534
|
if (e.isDirectory()) {
|
|
8865
9535
|
out.push(...await walkJson(full));
|
|
8866
9536
|
} else if (e.name.endsWith(".json") && e.name !== "index.json" && e.name !== "schema.json") {
|
|
@@ -8935,7 +9605,7 @@ function escapeRegExp(s) {
|
|
|
8935
9605
|
|
|
8936
9606
|
// src/storage/prompt-usage-store.ts
|
|
8937
9607
|
init_atomic_write();
|
|
8938
|
-
import * as
|
|
9608
|
+
import * as fs12 from "node:fs/promises";
|
|
8939
9609
|
var PromptUsageStore = class {
|
|
8940
9610
|
constructor(file) {
|
|
8941
9611
|
this.file = file;
|
|
@@ -8955,7 +9625,7 @@ var PromptUsageStore = class {
|
|
|
8955
9625
|
return this.cachedUsage;
|
|
8956
9626
|
}
|
|
8957
9627
|
try {
|
|
8958
|
-
const raw = JSON.parse(await
|
|
9628
|
+
const raw = JSON.parse(await fs12.readFile(this.file, "utf8"));
|
|
8959
9629
|
if (raw && typeof raw === "object" && raw.usage && typeof raw.usage === "object") {
|
|
8960
9630
|
this.cachedUsage = raw.usage;
|
|
8961
9631
|
this.cachedSignature = signature;
|
|
@@ -9035,8 +9705,8 @@ var PromptUsageStore = class {
|
|
|
9035
9705
|
}
|
|
9036
9706
|
async fileSignature() {
|
|
9037
9707
|
try {
|
|
9038
|
-
const
|
|
9039
|
-
return { size:
|
|
9708
|
+
const stat8 = await fs12.stat(this.file);
|
|
9709
|
+
return { size: stat8.size, mtimeMs: stat8.mtimeMs, ctimeMs: stat8.ctimeMs };
|
|
9040
9710
|
} catch {
|
|
9041
9711
|
return null;
|
|
9042
9712
|
}
|
|
@@ -9222,7 +9892,7 @@ ${exact.content}` };
|
|
|
9222
9892
|
2
|
|
9223
9893
|
);
|
|
9224
9894
|
try {
|
|
9225
|
-
await
|
|
9895
|
+
await fs13.writeFile(target, payload, "utf8");
|
|
9226
9896
|
return { message: `Exported ${own.length} prompt(s) \u2192 ${target}` };
|
|
9227
9897
|
} catch (err) {
|
|
9228
9898
|
return { message: `Export failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -9234,7 +9904,7 @@ ${exact.content}` };
|
|
|
9234
9904
|
const src = resolveIoPath(restJoined, ctx);
|
|
9235
9905
|
let raw;
|
|
9236
9906
|
try {
|
|
9237
|
-
raw = JSON.parse(await
|
|
9907
|
+
raw = JSON.parse(await fs13.readFile(src, "utf8"));
|
|
9238
9908
|
} catch (err) {
|
|
9239
9909
|
return { message: `Import failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
9240
9910
|
}
|
|
@@ -9440,9 +10110,9 @@ ${dim(`slug: ${entry.slug} | category: ${entry.category} | source: ${entry.sourc
|
|
|
9440
10110
|
}
|
|
9441
10111
|
function resolveIoPath(p, ctx) {
|
|
9442
10112
|
const cleaned = p.trim().replace(/^["']|["']$/g, "");
|
|
9443
|
-
if (
|
|
10113
|
+
if (path21.isAbsolute(cleaned)) return cleaned;
|
|
9444
10114
|
const base = ctx.projectRoot ?? process.cwd();
|
|
9445
|
-
return
|
|
10115
|
+
return path21.resolve(base, cleaned);
|
|
9446
10116
|
}
|
|
9447
10117
|
function sourceGlyph(e) {
|
|
9448
10118
|
return e.source === "project" ? "\u{1F4C1}" : e.source === "user" ? "\u{1F464}" : e.source === "synced" ? "\u2601" : "\u{1F4E6}";
|
|
@@ -9566,8 +10236,8 @@ async function integrateFindings(payload, projectDir, reportId) {
|
|
|
9566
10236
|
init_atomic_write();
|
|
9567
10237
|
init_review_finding_store();
|
|
9568
10238
|
init_review_report_store();
|
|
9569
|
-
import * as
|
|
9570
|
-
import * as
|
|
10239
|
+
import * as fsp6 from "node:fs/promises";
|
|
10240
|
+
import * as path22 from "node:path";
|
|
9571
10241
|
var REVIEW_STORE_COMPACTION_THRESHOLD_BYTES = 8 * 1024 * 1024;
|
|
9572
10242
|
var REVIEW_STORE_COMPACTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
9573
10243
|
var REVIEW_STORE_MAINTENANCE_FILE = ".review-store-maintenance.json";
|
|
@@ -9575,7 +10245,7 @@ async function maybeCompactReviewStores(projectDir, opts = {}) {
|
|
|
9575
10245
|
const thresholdBytes = opts.thresholdBytes ?? REVIEW_STORE_COMPACTION_THRESHOLD_BYTES;
|
|
9576
10246
|
const intervalMs = opts.intervalMs ?? REVIEW_STORE_COMPACTION_INTERVAL_MS;
|
|
9577
10247
|
const now = opts.now ?? Date.now();
|
|
9578
|
-
const maintenancePath =
|
|
10248
|
+
const maintenancePath = path22.join(projectDir, REVIEW_STORE_MAINTENANCE_FILE);
|
|
9579
10249
|
return withFileLock(maintenancePath, async () => {
|
|
9580
10250
|
const totalBytes = await fileSize(resolveReportStorePath(projectDir)) + await fileSize(resolveFindingStorePath(projectDir));
|
|
9581
10251
|
if (totalBytes < thresholdBytes) {
|
|
@@ -9615,7 +10285,7 @@ function emptyResult(reason, totalBytes) {
|
|
|
9615
10285
|
}
|
|
9616
10286
|
async function fileSize(filePath) {
|
|
9617
10287
|
try {
|
|
9618
|
-
return (await
|
|
10288
|
+
return (await fsp6.stat(filePath)).size;
|
|
9619
10289
|
} catch (error) {
|
|
9620
10290
|
if (error.code === "ENOENT") return 0;
|
|
9621
10291
|
throw error;
|
|
@@ -9623,7 +10293,7 @@ async function fileSize(filePath) {
|
|
|
9623
10293
|
}
|
|
9624
10294
|
async function readMaintenanceState(filePath) {
|
|
9625
10295
|
try {
|
|
9626
|
-
const parsed = JSON.parse(await
|
|
10296
|
+
const parsed = JSON.parse(await fsp6.readFile(filePath, "utf8"));
|
|
9627
10297
|
return typeof parsed.compactedAt === "string" && Number.isFinite(parsed.totalBytesBefore) ? parsed : null;
|
|
9628
10298
|
} catch (error) {
|
|
9629
10299
|
if (error.code === "ENOENT" || error instanceof SyntaxError) {
|
|
@@ -9635,7 +10305,7 @@ async function readMaintenanceState(filePath) {
|
|
|
9635
10305
|
|
|
9636
10306
|
// src/plugins/skills-plugin.ts
|
|
9637
10307
|
import * as os7 from "node:os";
|
|
9638
|
-
import * as
|
|
10308
|
+
import * as path27 from "node:path";
|
|
9639
10309
|
|
|
9640
10310
|
// src/skills/foreign-sources.ts
|
|
9641
10311
|
var FOREIGN_SKILL_TOOLS = [
|
|
@@ -9817,8 +10487,8 @@ function numField(rec, key) {
|
|
|
9817
10487
|
// src/skills/skill-generator.ts
|
|
9818
10488
|
init_errors();
|
|
9819
10489
|
import { spawn as spawn5 } from "node:child_process";
|
|
9820
|
-
import * as
|
|
9821
|
-
import * as
|
|
10490
|
+
import * as fs14 from "node:fs/promises";
|
|
10491
|
+
import * as path23 from "node:path";
|
|
9822
10492
|
async function validateSkillNameAvailable(name, loader) {
|
|
9823
10493
|
const formatViolations = validateSkillName(name);
|
|
9824
10494
|
const conflicts = loader ? (await loader.listEntries()).filter((e) => e.name === name) : [];
|
|
@@ -9923,7 +10593,12 @@ async function openInEditor(filePath, env = process.env) {
|
|
|
9923
10593
|
const child = spawn5(parts[0], [...parts.slice(1), filePath], {
|
|
9924
10594
|
stdio: "ignore",
|
|
9925
10595
|
detached: true,
|
|
9926
|
-
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
|
|
9927
10602
|
});
|
|
9928
10603
|
child.unref();
|
|
9929
10604
|
}
|
|
@@ -9937,11 +10612,11 @@ async function writeSkeletonSkill(skillsDir, body, opts = {}) {
|
|
|
9937
10612
|
subsystem: "general"
|
|
9938
10613
|
});
|
|
9939
10614
|
}
|
|
9940
|
-
const skillDir =
|
|
9941
|
-
const skillFile =
|
|
10615
|
+
const skillDir = path23.join(skillsDir, name);
|
|
10616
|
+
const skillFile = path23.join(skillDir, "SKILL.md");
|
|
9942
10617
|
if (!opts.overwrite) {
|
|
9943
10618
|
try {
|
|
9944
|
-
await
|
|
10619
|
+
await fs14.access(skillFile);
|
|
9945
10620
|
throw new WrongStackError({
|
|
9946
10621
|
message: `A skill already exists at ${skillFile}. Use --force to overwrite.`,
|
|
9947
10622
|
code: ERROR_CODES.VALIDATION_ERROR,
|
|
@@ -9952,8 +10627,8 @@ async function writeSkeletonSkill(skillsDir, body, opts = {}) {
|
|
|
9952
10627
|
if (err instanceof WrongStackError) throw err;
|
|
9953
10628
|
}
|
|
9954
10629
|
}
|
|
9955
|
-
await
|
|
9956
|
-
await
|
|
10630
|
+
await fs14.mkdir(skillDir, { recursive: true });
|
|
10631
|
+
await fs14.writeFile(skillFile, body, "utf8");
|
|
9957
10632
|
return skillFile;
|
|
9958
10633
|
}
|
|
9959
10634
|
function bodyLineAdvisory(body) {
|
|
@@ -9996,15 +10671,15 @@ function defaultEditor() {
|
|
|
9996
10671
|
// src/skills/skill-installer.ts
|
|
9997
10672
|
init_errors();
|
|
9998
10673
|
init_error();
|
|
9999
|
-
import * as
|
|
10000
|
-
import * as
|
|
10674
|
+
import * as fs17 from "node:fs/promises";
|
|
10675
|
+
import * as path26 from "node:path";
|
|
10001
10676
|
|
|
10002
10677
|
// src/skills/github-fetcher.ts
|
|
10003
10678
|
init_errors();
|
|
10004
10679
|
import { createWriteStream } from "node:fs";
|
|
10005
|
-
import * as
|
|
10680
|
+
import * as fs15 from "node:fs/promises";
|
|
10006
10681
|
import * as os6 from "node:os";
|
|
10007
|
-
import * as
|
|
10682
|
+
import * as path24 from "node:path";
|
|
10008
10683
|
import { Readable } from "node:stream";
|
|
10009
10684
|
import { pipeline } from "node:stream/promises";
|
|
10010
10685
|
import { createGunzip } from "node:zlib";
|
|
@@ -10146,7 +10821,7 @@ async function downloadGitHubTarball(parsed) {
|
|
|
10146
10821
|
}
|
|
10147
10822
|
});
|
|
10148
10823
|
}
|
|
10149
|
-
const tempDir = await
|
|
10824
|
+
const tempDir = await fs15.mkdtemp(path24.join(os6.tmpdir(), "wskill-"));
|
|
10150
10825
|
try {
|
|
10151
10826
|
if (!response.body) {
|
|
10152
10827
|
throw new WrongStackError({
|
|
@@ -10157,17 +10832,17 @@ async function downloadGitHubTarball(parsed) {
|
|
|
10157
10832
|
});
|
|
10158
10833
|
}
|
|
10159
10834
|
const nodeStream = Readable.fromWeb(response.body);
|
|
10160
|
-
const tarPath =
|
|
10835
|
+
const tarPath = path24.join(tempDir, ".wrongstack-download.tar");
|
|
10161
10836
|
await writeBoundedGzipStream(nodeStream, tarPath, SKILL_LIMITS.MAX_UNCOMPRESSED_TARBALL_SIZE);
|
|
10162
|
-
const tarBuf = await
|
|
10837
|
+
const tarBuf = await fs15.readFile(tarPath);
|
|
10163
10838
|
try {
|
|
10164
10839
|
await extractTar(tarBuf, tempDir);
|
|
10165
10840
|
} finally {
|
|
10166
|
-
await
|
|
10841
|
+
await fs15.rm(tarPath, { force: true });
|
|
10167
10842
|
}
|
|
10168
10843
|
return { tempDir };
|
|
10169
10844
|
} catch (err) {
|
|
10170
|
-
await
|
|
10845
|
+
await fs15.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
10171
10846
|
});
|
|
10172
10847
|
throw err;
|
|
10173
10848
|
}
|
|
@@ -10222,25 +10897,25 @@ async function extractTar(buf, destDir) {
|
|
|
10222
10897
|
const fullPath = prefix ? `${prefix}/${name}` : name;
|
|
10223
10898
|
const relPath = stripTopDir(fullPath);
|
|
10224
10899
|
if (relPath && relPath !== "." && relPath !== "..") {
|
|
10225
|
-
const destPath =
|
|
10226
|
-
const resolvedDest =
|
|
10227
|
-
const resolvedRoot =
|
|
10228
|
-
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)) {
|
|
10229
10904
|
offset += 512 + Math.ceil(size / 512) * 512;
|
|
10230
10905
|
continue;
|
|
10231
10906
|
}
|
|
10232
10907
|
if (typeflag === 53 || typeflag === 0) {
|
|
10233
10908
|
if (relPath.endsWith("/") || typeflag === 53) {
|
|
10234
|
-
await
|
|
10909
|
+
await fs15.mkdir(destPath, { recursive: true });
|
|
10235
10910
|
}
|
|
10236
10911
|
}
|
|
10237
10912
|
if ((typeflag === 48 || typeflag === 0 || typeflag === 0) && size > 0) {
|
|
10238
|
-
const dir =
|
|
10239
|
-
await
|
|
10913
|
+
const dir = path24.dirname(destPath);
|
|
10914
|
+
await fs15.mkdir(dir, { recursive: true });
|
|
10240
10915
|
const dataStart = offset + 512;
|
|
10241
10916
|
const dataEnd = dataStart + size;
|
|
10242
10917
|
if (dataEnd > buf.length) break;
|
|
10243
|
-
await
|
|
10918
|
+
await fs15.writeFile(destPath, buf.subarray(dataStart, dataEnd));
|
|
10244
10919
|
}
|
|
10245
10920
|
}
|
|
10246
10921
|
offset += 512 + Math.ceil(size / 512) * 512;
|
|
@@ -10249,8 +10924,8 @@ async function extractTar(buf, destDir) {
|
|
|
10249
10924
|
|
|
10250
10925
|
// src/skills/manifest-store.ts
|
|
10251
10926
|
init_atomic_write();
|
|
10252
|
-
import * as
|
|
10253
|
-
import * as
|
|
10927
|
+
import * as fs16 from "node:fs/promises";
|
|
10928
|
+
import * as path25 from "node:path";
|
|
10254
10929
|
var SkillManifestStore = class {
|
|
10255
10930
|
manifestPath;
|
|
10256
10931
|
cache;
|
|
@@ -10260,7 +10935,7 @@ var SkillManifestStore = class {
|
|
|
10260
10935
|
async read() {
|
|
10261
10936
|
if (this.cache) return this.cache;
|
|
10262
10937
|
try {
|
|
10263
|
-
const raw = await
|
|
10938
|
+
const raw = await fs16.readFile(this.manifestPath, "utf8");
|
|
10264
10939
|
const data = JSON.parse(raw);
|
|
10265
10940
|
if (!Array.isArray(data.skills)) {
|
|
10266
10941
|
this.cache = { skills: [] };
|
|
@@ -10273,8 +10948,8 @@ var SkillManifestStore = class {
|
|
|
10273
10948
|
return this.cache;
|
|
10274
10949
|
}
|
|
10275
10950
|
async write(data) {
|
|
10276
|
-
const dir =
|
|
10277
|
-
await
|
|
10951
|
+
const dir = path25.dirname(this.manifestPath);
|
|
10952
|
+
await fs16.mkdir(dir, { recursive: true });
|
|
10278
10953
|
await atomicWrite(this.manifestPath, JSON.stringify(data, null, 2) + "\n");
|
|
10279
10954
|
this.cache = data;
|
|
10280
10955
|
}
|
|
@@ -10312,8 +10987,8 @@ var SkillManifestStore = class {
|
|
|
10312
10987
|
|
|
10313
10988
|
// src/skills/skill-installer.ts
|
|
10314
10989
|
function isInside(resolved, destDir) {
|
|
10315
|
-
const root =
|
|
10316
|
-
return resolved === root || resolved.startsWith(root +
|
|
10990
|
+
const root = path26.resolve(destDir);
|
|
10991
|
+
return resolved === root || resolved.startsWith(root + path26.sep);
|
|
10317
10992
|
}
|
|
10318
10993
|
var MAX_SKILL_FILE_SIZE = SKILL_LIMITS.MAX_SKILL_FILE_SIZE;
|
|
10319
10994
|
var SkillInstaller = class {
|
|
@@ -10368,13 +11043,13 @@ var SkillInstaller = class {
|
|
|
10368
11043
|
this.opts.log?.(`Overwriting existing skill "${skill.name}" (${scope})...`);
|
|
10369
11044
|
await this.removeSkillFiles(skill.name, scope);
|
|
10370
11045
|
}
|
|
10371
|
-
const destDir =
|
|
10372
|
-
await
|
|
11046
|
+
const destDir = path26.join(targetDir, skill.name);
|
|
11047
|
+
await fs17.mkdir(destDir, { recursive: true });
|
|
10373
11048
|
const copiedFiles = [];
|
|
10374
11049
|
for (const file of skill.files) {
|
|
10375
|
-
const srcPath =
|
|
10376
|
-
const destPath =
|
|
10377
|
-
const resolved2 =
|
|
11050
|
+
const srcPath = path26.join(skill.baseDir, file);
|
|
11051
|
+
const destPath = path26.join(destDir, file);
|
|
11052
|
+
const resolved2 = path26.resolve(destPath);
|
|
10378
11053
|
if (!isInside(resolved2, destDir)) {
|
|
10379
11054
|
throw new FsError({
|
|
10380
11055
|
message: `Path traversal detected in skill file: ${file}`,
|
|
@@ -10383,17 +11058,17 @@ var SkillInstaller = class {
|
|
|
10383
11058
|
context: { reason: "path_traversal", skillName: skill.name }
|
|
10384
11059
|
});
|
|
10385
11060
|
}
|
|
10386
|
-
const
|
|
10387
|
-
if (
|
|
11061
|
+
const stat8 = await fs17.stat(srcPath);
|
|
11062
|
+
if (stat8.size > MAX_SKILL_FILE_SIZE) {
|
|
10388
11063
|
throw new FsError({
|
|
10389
|
-
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`,
|
|
10390
11065
|
code: ERROR_CODES.FS_WRITE_FAILED,
|
|
10391
11066
|
path: srcPath,
|
|
10392
|
-
context: { skillName: skill.name, fileSize:
|
|
11067
|
+
context: { skillName: skill.name, fileSize: stat8.size, maxSize: MAX_SKILL_FILE_SIZE }
|
|
10393
11068
|
});
|
|
10394
11069
|
}
|
|
10395
|
-
await
|
|
10396
|
-
await
|
|
11070
|
+
await fs17.mkdir(path26.dirname(destPath), { recursive: true });
|
|
11071
|
+
await fs17.copyFile(srcPath, destPath);
|
|
10397
11072
|
copiedFiles.push(file);
|
|
10398
11073
|
}
|
|
10399
11074
|
const entry = {
|
|
@@ -10423,7 +11098,7 @@ var SkillInstaller = class {
|
|
|
10423
11098
|
this.invalidateLoaderCache();
|
|
10424
11099
|
return results;
|
|
10425
11100
|
} finally {
|
|
10426
|
-
await
|
|
11101
|
+
await fs17.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
10427
11102
|
});
|
|
10428
11103
|
}
|
|
10429
11104
|
}
|
|
@@ -10438,7 +11113,7 @@ var SkillInstaller = class {
|
|
|
10438
11113
|
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
10439
11114
|
let entries;
|
|
10440
11115
|
try {
|
|
10441
|
-
entries = await
|
|
11116
|
+
entries = await fs17.readdir(srcDir, { withFileTypes: true });
|
|
10442
11117
|
} catch {
|
|
10443
11118
|
throw new WrongStackError({
|
|
10444
11119
|
message: `Source directory not found or not readable: ${srcDir}`,
|
|
@@ -10450,10 +11125,10 @@ var SkillInstaller = class {
|
|
|
10450
11125
|
const results = [];
|
|
10451
11126
|
for (const e of entries) {
|
|
10452
11127
|
if (!await entryIsDirectory(srcDir, e)) continue;
|
|
10453
|
-
const skillMdPath =
|
|
11128
|
+
const skillMdPath = path26.join(srcDir, e.name, "SKILL.md");
|
|
10454
11129
|
let content;
|
|
10455
11130
|
try {
|
|
10456
|
-
content = await
|
|
11131
|
+
content = await fs17.readFile(skillMdPath, "utf8");
|
|
10457
11132
|
} catch {
|
|
10458
11133
|
continue;
|
|
10459
11134
|
}
|
|
@@ -10463,15 +11138,15 @@ var SkillInstaller = class {
|
|
|
10463
11138
|
if (existing.find((x) => x.scope === scope)) {
|
|
10464
11139
|
await this.removeSkillFiles(fm.name, scope);
|
|
10465
11140
|
}
|
|
10466
|
-
const destDir =
|
|
10467
|
-
await
|
|
10468
|
-
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);
|
|
10469
11144
|
const files = await collectFiles(srcSkillDir, srcSkillDir);
|
|
10470
11145
|
const copiedFiles = [];
|
|
10471
11146
|
for (const file of files) {
|
|
10472
|
-
const srcPath =
|
|
10473
|
-
const destPath =
|
|
10474
|
-
const resolved =
|
|
11147
|
+
const srcPath = path26.join(srcSkillDir, file);
|
|
11148
|
+
const destPath = path26.join(destDir, file);
|
|
11149
|
+
const resolved = path26.resolve(destPath);
|
|
10475
11150
|
if (!isInside(resolved, destDir)) {
|
|
10476
11151
|
throw new FsError({
|
|
10477
11152
|
message: `Path traversal detected in skill file: ${file}`,
|
|
@@ -10480,16 +11155,16 @@ var SkillInstaller = class {
|
|
|
10480
11155
|
context: { reason: "path_traversal", skillName: fm.name }
|
|
10481
11156
|
});
|
|
10482
11157
|
}
|
|
10483
|
-
await
|
|
11158
|
+
await fs17.mkdir(path26.dirname(destPath), { recursive: true });
|
|
10484
11159
|
if (opts?.link) {
|
|
10485
11160
|
try {
|
|
10486
|
-
await
|
|
11161
|
+
await fs17.symlink(srcPath, destPath);
|
|
10487
11162
|
} catch (err) {
|
|
10488
|
-
await
|
|
11163
|
+
await fs17.copyFile(srcPath, destPath);
|
|
10489
11164
|
this.opts.log?.(`symlink failed for ${file} (${toErrorMessage(err)}); copied instead`);
|
|
10490
11165
|
}
|
|
10491
11166
|
} else {
|
|
10492
|
-
await
|
|
11167
|
+
await fs17.copyFile(srcPath, destPath);
|
|
10493
11168
|
}
|
|
10494
11169
|
copiedFiles.push(file);
|
|
10495
11170
|
}
|
|
@@ -10643,10 +11318,10 @@ var SkillInstaller = class {
|
|
|
10643
11318
|
*/
|
|
10644
11319
|
async detectSkills(baseDir) {
|
|
10645
11320
|
const results = [];
|
|
10646
|
-
const rootSkillMd =
|
|
11321
|
+
const rootSkillMd = path26.join(baseDir, "SKILL.md");
|
|
10647
11322
|
try {
|
|
10648
|
-
await
|
|
10649
|
-
const content = await
|
|
11323
|
+
await fs17.access(rootSkillMd);
|
|
11324
|
+
const content = await fs17.readFile(rootSkillMd, "utf8");
|
|
10650
11325
|
const fm = parseSkillFrontmatter(content);
|
|
10651
11326
|
if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
|
|
10652
11327
|
results.push({
|
|
@@ -10658,17 +11333,17 @@ var SkillInstaller = class {
|
|
|
10658
11333
|
}
|
|
10659
11334
|
} catch {
|
|
10660
11335
|
}
|
|
10661
|
-
const skillsDir =
|
|
11336
|
+
const skillsDir = path26.join(baseDir, "skills");
|
|
10662
11337
|
try {
|
|
10663
|
-
const entries = await
|
|
11338
|
+
const entries = await fs17.readdir(skillsDir, { withFileTypes: true });
|
|
10664
11339
|
for (const entry of entries) {
|
|
10665
11340
|
if (!entry.isDirectory()) continue;
|
|
10666
|
-
const skillFile =
|
|
11341
|
+
const skillFile = path26.join(skillsDir, entry.name, "SKILL.md");
|
|
10667
11342
|
try {
|
|
10668
|
-
const content = await
|
|
11343
|
+
const content = await fs17.readFile(skillFile, "utf8");
|
|
10669
11344
|
const fm = parseSkillFrontmatter(content);
|
|
10670
11345
|
if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
|
|
10671
|
-
const skillDir =
|
|
11346
|
+
const skillDir = path26.join(skillsDir, entry.name);
|
|
10672
11347
|
const files = await collectFiles(skillDir, skillDir);
|
|
10673
11348
|
results.push({
|
|
10674
11349
|
name: fm.name,
|
|
@@ -10701,8 +11376,8 @@ var SkillInstaller = class {
|
|
|
10701
11376
|
*/
|
|
10702
11377
|
async removeSkillFiles(name, scope) {
|
|
10703
11378
|
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
10704
|
-
const root =
|
|
10705
|
-
const skillDir =
|
|
11379
|
+
const root = path26.resolve(targetDir);
|
|
11380
|
+
const skillDir = path26.resolve(path26.join(targetDir, name));
|
|
10706
11381
|
if (skillDir === root || !isInside(skillDir, root)) {
|
|
10707
11382
|
throw new FsError({
|
|
10708
11383
|
message: `Refusing to delete skill files outside the skills directory: ${name}`,
|
|
@@ -10711,7 +11386,7 @@ var SkillInstaller = class {
|
|
|
10711
11386
|
context: { reason: "path_traversal", skillName: name, scope }
|
|
10712
11387
|
});
|
|
10713
11388
|
}
|
|
10714
|
-
await
|
|
11389
|
+
await fs17.rm(skillDir, { recursive: true, force: true });
|
|
10715
11390
|
}
|
|
10716
11391
|
/**
|
|
10717
11392
|
* Invalidate the skill loader's cache so newly installed skills appear.
|
|
@@ -10746,7 +11421,7 @@ async function entryIsDirectory(dir, entry) {
|
|
|
10746
11421
|
if (entry.isDirectory()) return true;
|
|
10747
11422
|
if (entry.isSymbolicLink()) {
|
|
10748
11423
|
try {
|
|
10749
|
-
return (await
|
|
11424
|
+
return (await fs17.stat(path26.join(dir, entry.name))).isDirectory();
|
|
10750
11425
|
} catch {
|
|
10751
11426
|
return false;
|
|
10752
11427
|
}
|
|
@@ -10755,10 +11430,10 @@ async function entryIsDirectory(dir, entry) {
|
|
|
10755
11430
|
}
|
|
10756
11431
|
async function collectFiles(dir, baseDir) {
|
|
10757
11432
|
const results = [];
|
|
10758
|
-
const entries = await
|
|
11433
|
+
const entries = await fs17.readdir(dir, { withFileTypes: true });
|
|
10759
11434
|
for (const entry of entries) {
|
|
10760
|
-
const fullPath =
|
|
10761
|
-
const relPath =
|
|
11435
|
+
const fullPath = path26.join(dir, entry.name);
|
|
11436
|
+
const relPath = path26.relative(baseDir, fullPath);
|
|
10762
11437
|
if (entry.isDirectory()) {
|
|
10763
11438
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
10764
11439
|
results.push(...await collectFiles(fullPath, baseDir));
|
|
@@ -10837,7 +11512,7 @@ function createSkillsPlugin(opts) {
|
|
|
10837
11512
|
function makeInstaller(skillLoader, projectRoot, registryAdapters) {
|
|
10838
11513
|
const paths = resolveWstackPaths({ projectRoot });
|
|
10839
11514
|
return new SkillInstaller({
|
|
10840
|
-
manifestPath:
|
|
11515
|
+
manifestPath: path27.join(paths.configDir, "installed-skills.json"),
|
|
10841
11516
|
projectSkillsDir: paths.inProjectSkills,
|
|
10842
11517
|
globalSkillsDir: paths.globalSkills,
|
|
10843
11518
|
projectHash: paths.projectHash,
|
|
@@ -11216,7 +11891,7 @@ function resolveImportSourceDir(tool, opts) {
|
|
|
11216
11891
|
const entry = IMPORT_SOURCE_TOOLS.find((t) => t.id === tool);
|
|
11217
11892
|
if (!entry) return void 0;
|
|
11218
11893
|
const base = opts.global ? opts.homeDir ?? os7.homedir() : opts.projectRoot;
|
|
11219
|
-
return
|
|
11894
|
+
return path27.join(base, "." + entry.id, entry.subdir);
|
|
11220
11895
|
}
|
|
11221
11896
|
function buildSkillImportCommand(skillLoader) {
|
|
11222
11897
|
return {
|
|
@@ -11257,7 +11932,7 @@ function buildSkillImportCommand(skillLoader) {
|
|
|
11257
11932
|
};
|
|
11258
11933
|
}
|
|
11259
11934
|
} else if (positional) {
|
|
11260
|
-
srcDir =
|
|
11935
|
+
srcDir = path27.resolve(ctx.projectRoot, positional);
|
|
11261
11936
|
} else {
|
|
11262
11937
|
return {
|
|
11263
11938
|
message: "Usage: /skill-import <src-dir> | --from <tool> | --from-claude [--global] [--link]"
|
|
@@ -11384,14 +12059,14 @@ function truncate(s, max) {
|
|
|
11384
12059
|
}
|
|
11385
12060
|
|
|
11386
12061
|
// src/plugins/sync-plugin.ts
|
|
11387
|
-
import * as
|
|
12062
|
+
import * as path29 from "node:path";
|
|
11388
12063
|
init_error();
|
|
11389
12064
|
|
|
11390
12065
|
// src/storage/cloud-sync.ts
|
|
11391
12066
|
init_atomic_write();
|
|
11392
12067
|
init_errors();
|
|
11393
|
-
import * as
|
|
11394
|
-
import * as
|
|
12068
|
+
import * as fs18 from "node:fs/promises";
|
|
12069
|
+
import * as path28 from "node:path";
|
|
11395
12070
|
import { createHash as createHash9 } from "node:crypto";
|
|
11396
12071
|
var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
|
|
11397
12072
|
var CloudSync = class {
|
|
@@ -11399,8 +12074,8 @@ var CloudSync = class {
|
|
|
11399
12074
|
this.paths = paths;
|
|
11400
12075
|
this.getConfig = getConfig;
|
|
11401
12076
|
this.setConfig = setConfig;
|
|
11402
|
-
this.statePath =
|
|
11403
|
-
this.getSettingsConfigPath = getSettingsConfigPath ?? (() =>
|
|
12077
|
+
this.statePath = path28.join(paths.configDir, "sync-state.json");
|
|
12078
|
+
this.getSettingsConfigPath = getSettingsConfigPath ?? (() => path28.join(this.paths.configDir, "config.json"));
|
|
11404
12079
|
}
|
|
11405
12080
|
paths;
|
|
11406
12081
|
getConfig;
|
|
@@ -11555,7 +12230,7 @@ var CloudSync = class {
|
|
|
11555
12230
|
}
|
|
11556
12231
|
async loadState() {
|
|
11557
12232
|
try {
|
|
11558
|
-
const raw = await
|
|
12233
|
+
const raw = await fs18.readFile(this.statePath, "utf8");
|
|
11559
12234
|
this.state = JSON.parse(raw);
|
|
11560
12235
|
} catch {
|
|
11561
12236
|
this.state = null;
|
|
@@ -11680,18 +12355,18 @@ var CloudSync = class {
|
|
|
11680
12355
|
const localPath = this.categoryToPath(cat);
|
|
11681
12356
|
if (!localPath) continue;
|
|
11682
12357
|
try {
|
|
11683
|
-
const
|
|
11684
|
-
if (
|
|
11685
|
-
if (
|
|
12358
|
+
const stat8 = await fs18.lstat(localPath);
|
|
12359
|
+
if (stat8.isSymbolicLink()) continue;
|
|
12360
|
+
if (stat8.isDirectory()) {
|
|
11686
12361
|
const files = await this.walkDir(localPath, localPath);
|
|
11687
12362
|
for (const file of files) {
|
|
11688
|
-
const content = await
|
|
11689
|
-
const rel =
|
|
12363
|
+
const content = await fs18.readFile(file, "utf8");
|
|
12364
|
+
const rel = path28.relative(localPath, file).replace(/\\/g, "/");
|
|
11690
12365
|
entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
|
|
11691
12366
|
hashes.push(`${cat}/${rel}\0${content}`);
|
|
11692
12367
|
}
|
|
11693
12368
|
} else {
|
|
11694
|
-
const content = await
|
|
12369
|
+
const content = await fs18.readFile(localPath, "utf8");
|
|
11695
12370
|
entries.push({ path: `data/${cat}`, content, mode: "100644" });
|
|
11696
12371
|
hashes.push(`${cat}\0${content}`);
|
|
11697
12372
|
}
|
|
@@ -11707,17 +12382,17 @@ var CloudSync = class {
|
|
|
11707
12382
|
const localPath = this.categoryToPath(cat);
|
|
11708
12383
|
if (!localPath) continue;
|
|
11709
12384
|
try {
|
|
11710
|
-
const
|
|
11711
|
-
if (
|
|
11712
|
-
if (
|
|
12385
|
+
const stat8 = await fs18.lstat(localPath);
|
|
12386
|
+
if (stat8.isSymbolicLink()) continue;
|
|
12387
|
+
if (stat8.isDirectory()) {
|
|
11713
12388
|
const files = await this.walkDir(localPath, localPath);
|
|
11714
12389
|
for (const file of files) {
|
|
11715
|
-
const content = await
|
|
11716
|
-
const rel =
|
|
12390
|
+
const content = await fs18.readFile(file, "utf8");
|
|
12391
|
+
const rel = path28.relative(localPath, file).replace(/\\/g, "/");
|
|
11717
12392
|
hashes.push(`${cat}/${rel}\0${content}`);
|
|
11718
12393
|
}
|
|
11719
12394
|
} else {
|
|
11720
|
-
const content = await
|
|
12395
|
+
const content = await fs18.readFile(localPath, "utf8");
|
|
11721
12396
|
hashes.push(`${cat}\0${content}`);
|
|
11722
12397
|
}
|
|
11723
12398
|
} catch {
|
|
@@ -11744,10 +12419,10 @@ var CloudSync = class {
|
|
|
11744
12419
|
}
|
|
11745
12420
|
async walkDir(dir, base) {
|
|
11746
12421
|
const results = [];
|
|
11747
|
-
const entries = await
|
|
12422
|
+
const entries = await fs18.readdir(dir, { withFileTypes: true });
|
|
11748
12423
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
11749
12424
|
for (const entry of entries) {
|
|
11750
|
-
const full =
|
|
12425
|
+
const full = path28.join(dir, entry.name);
|
|
11751
12426
|
if (entry.isSymbolicLink()) continue;
|
|
11752
12427
|
if (entry.isDirectory()) {
|
|
11753
12428
|
results.push(...await this.walkDir(full, base));
|
|
@@ -11760,29 +12435,29 @@ var CloudSync = class {
|
|
|
11760
12435
|
};
|
|
11761
12436
|
async function preparePulledDestination(cat, localPath, destPath, remotePath) {
|
|
11762
12437
|
const directoryBacked = cat === "skills" || cat === "prompts";
|
|
11763
|
-
const rootPath = directoryBacked ? localPath :
|
|
12438
|
+
const rootPath = directoryBacked ? localPath : path28.dirname(localPath);
|
|
11764
12439
|
const rootStat = await lstatIfExists(rootPath);
|
|
11765
12440
|
if (rootStat?.isSymbolicLink()) {
|
|
11766
12441
|
throw unsafePulledSymlinkError(remotePath, rootPath);
|
|
11767
12442
|
}
|
|
11768
12443
|
if (directoryBacked) {
|
|
11769
|
-
const relativeParent =
|
|
12444
|
+
const relativeParent = path28.relative(rootPath, path28.dirname(destPath));
|
|
11770
12445
|
let cursor = rootPath;
|
|
11771
12446
|
if (relativeParent) {
|
|
11772
|
-
for (const segment of relativeParent.split(
|
|
11773
|
-
cursor =
|
|
11774
|
-
const
|
|
11775
|
-
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);
|
|
11776
12451
|
}
|
|
11777
12452
|
}
|
|
11778
12453
|
}
|
|
11779
12454
|
const destStat = await lstatIfExists(destPath);
|
|
11780
12455
|
if (destStat?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, destPath);
|
|
11781
|
-
await
|
|
12456
|
+
await fs18.mkdir(path28.dirname(destPath), { recursive: true });
|
|
11782
12457
|
}
|
|
11783
12458
|
async function lstatIfExists(filePath) {
|
|
11784
12459
|
try {
|
|
11785
|
-
return await
|
|
12460
|
+
return await fs18.lstat(filePath);
|
|
11786
12461
|
} catch (err) {
|
|
11787
12462
|
if (err.code === "ENOENT") return null;
|
|
11788
12463
|
throw err;
|
|
@@ -11808,9 +12483,9 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
|
11808
12483
|
return localPath;
|
|
11809
12484
|
}
|
|
11810
12485
|
if (!rel) return localPath;
|
|
11811
|
-
const normalizedRel =
|
|
11812
|
-
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${
|
|
11813
|
-
if (
|
|
12486
|
+
const normalizedRel = path28.normalize(rel);
|
|
12487
|
+
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path28.sep}`);
|
|
12488
|
+
if (path28.isAbsolute(normalizedRel) || traversesUp) {
|
|
11814
12489
|
throw new FsError({
|
|
11815
12490
|
message: `Refusing CloudSync path traversal: ${remotePath}`,
|
|
11816
12491
|
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
@@ -11818,10 +12493,10 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
|
11818
12493
|
context: { reason: "path_traversal", normalizedRel }
|
|
11819
12494
|
});
|
|
11820
12495
|
}
|
|
11821
|
-
const dest =
|
|
11822
|
-
const root =
|
|
11823
|
-
const relative5 =
|
|
11824
|
-
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)) {
|
|
11825
12500
|
throw new FsError({
|
|
11826
12501
|
message: `Refusing CloudSync path outside category root: ${remotePath}`,
|
|
11827
12502
|
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
@@ -11864,7 +12539,7 @@ function createSyncPlugin(opts) {
|
|
|
11864
12539
|
api.log.warn("[sync] paths, configStore, or configDir not available \u2014 /sync disabled");
|
|
11865
12540
|
return;
|
|
11866
12541
|
}
|
|
11867
|
-
const syncConfigPath = paths.syncConfig ??
|
|
12542
|
+
const syncConfigPath = paths.syncConfig ?? path29.join(paths.configDir, "sync.json");
|
|
11868
12543
|
cloud = new CloudSync(
|
|
11869
12544
|
paths,
|
|
11870
12545
|
() => {
|
|
@@ -11875,7 +12550,7 @@ function createSyncPlugin(opts) {
|
|
|
11875
12550
|
await persistSyncConfig(syncConfigPath, cfg, vault);
|
|
11876
12551
|
configStore?.update({ sync: cfg });
|
|
11877
12552
|
},
|
|
11878
|
-
() =>
|
|
12553
|
+
() => path29.join(paths.configDir, "config.json")
|
|
11879
12554
|
);
|
|
11880
12555
|
void cloud.loadState();
|
|
11881
12556
|
api.slashCommands.register(buildSyncCommand(cloud, configStore, vault, syncConfigPath));
|
|
@@ -12065,6 +12740,852 @@ Warning: sync completed, but metadata persistence failed: ${toErrorMessage(err)}
|
|
|
12065
12740
|
}
|
|
12066
12741
|
};
|
|
12067
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
|
+
}
|
|
12068
13589
|
export {
|
|
12069
13590
|
CHIMERA_REVIEW_PROMPT,
|
|
12070
13591
|
DefaultPluginAPI,
|
|
@@ -12072,6 +13593,7 @@ export {
|
|
|
12072
13593
|
buildReviewerModelPool,
|
|
12073
13594
|
createAutoReviewPlugin,
|
|
12074
13595
|
createChimeraPlugin,
|
|
13596
|
+
createCloudConfigSyncPlugin,
|
|
12075
13597
|
createPromptsPlugin,
|
|
12076
13598
|
createSkillsPlugin,
|
|
12077
13599
|
createSyncPlugin,
|