@stackmemoryai/stackmemory 1.10.4 → 1.12.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/README.md +104 -23
- package/dist/src/cli/claude-sm.js +266 -84
- package/dist/src/cli/codex-sm.js +185 -33
- package/dist/src/cli/commands/bench.js +209 -2
- package/dist/src/cli/commands/cache.js +126 -0
- package/dist/src/cli/commands/daemon.js +41 -0
- package/dist/src/cli/commands/handoff.js +40 -9
- package/dist/src/cli/commands/onboard.js +70 -3
- package/dist/src/cli/commands/optimize.js +117 -0
- package/dist/src/cli/commands/orchestrate.js +230 -5
- package/dist/src/cli/commands/orchestrator.js +312 -24
- package/dist/src/cli/commands/pack.js +322 -0
- package/dist/src/cli/commands/search.js +40 -1
- package/dist/src/cli/commands/setup.js +177 -7
- package/dist/src/cli/commands/skills.js +10 -1
- package/dist/src/cli/commands/state.js +265 -0
- package/dist/src/cli/commands/wiki.js +33 -0
- package/dist/src/cli/gemini-sm.js +19 -29
- package/dist/src/cli/index.js +90 -29
- package/dist/src/cli/opencode-sm.js +38 -21
- package/dist/src/cli/utils/determinism-watcher.js +66 -0
- package/dist/src/cli/utils/real-cli-bin.js +44 -0
- package/dist/src/core/cache/content-cache.js +238 -0
- package/dist/src/core/cache/index.js +11 -0
- package/dist/src/core/cache/token-estimator.js +16 -0
- package/dist/src/core/context/frame-database.js +38 -30
- package/dist/src/core/cross-search/cross-project-search.js +269 -0
- package/dist/src/core/{merge → cross-search}/index.js +6 -4
- package/dist/src/core/database/sqlite-adapter.js +0 -83
- package/dist/src/core/extensions/provider-adapter.js +5 -0
- package/dist/src/core/models/model-router.js +22 -2
- package/dist/src/core/monitoring/logger.js +2 -1
- package/dist/src/core/optimization/trace-optimizer.js +413 -0
- package/dist/src/core/provenance/confidence-scorer.js +128 -0
- package/dist/src/core/provenance/index.js +40 -0
- package/dist/src/core/provenance/provenance-store.js +194 -0
- package/dist/src/core/provenance/types.js +82 -0
- package/dist/src/core/session/project-handoff.js +64 -0
- package/dist/src/core/session/session-manager.js +28 -0
- package/dist/src/core/shared-state/canonical-store.js +564 -0
- package/dist/src/core/skill-packs/index.js +18 -0
- package/dist/src/core/skill-packs/parser.js +42 -0
- package/dist/src/core/skill-packs/registry.js +224 -0
- package/dist/src/core/skill-packs/types.js +66 -0
- package/dist/src/core/trace/trace-event-store.js +282 -0
- package/dist/src/core/trace/trace-event.js +4 -0
- package/dist/src/core/wiki/wiki-compiler.js +219 -0
- package/dist/src/daemon/daemon-config.js +7 -0
- package/dist/src/daemon/services/github-service.js +126 -0
- package/dist/src/daemon/unified-daemon.js +30 -0
- package/dist/src/features/sweep/pty-wrapper.js +13 -5
- package/dist/src/hooks/schemas.js +2 -0
- package/dist/src/integrations/claude-code/subagent-client.js +89 -0
- package/dist/src/integrations/github/pr-state.js +158 -0
- package/dist/src/integrations/linear/client.js +4 -1
- package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +188 -0
- package/dist/src/integrations/mcp/handlers/index.js +40 -59
- package/dist/src/integrations/mcp/server.js +425 -311
- package/dist/src/integrations/mcp/tool-alias-registry.js +370 -0
- package/dist/src/integrations/mcp/tool-definitions.js +98 -229
- package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +3 -40
- package/dist/src/integrations/ralph/learning/pattern-learner.js +1 -20
- package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -2
- package/dist/src/mcp/stackmemory-mcp-server.js +315 -0
- package/dist/src/orchestrators/multimodal/determinism.js +243 -0
- package/dist/src/orchestrators/multimodal/harness.js +147 -77
- package/dist/src/orchestrators/multimodal/providers.js +44 -3
- package/dist/src/utils/hook-installer.js +8 -8
- package/package.json +10 -1
- package/packs/coding/python-fastapi/instructions.md +60 -0
- package/packs/coding/python-fastapi/pack.yaml +28 -0
- package/packs/coding/typescript-react/instructions.md +47 -0
- package/packs/coding/typescript-react/pack.yaml +28 -0
- package/packs/core/commands/capture.md +32 -0
- package/packs/core/commands/learn.md +73 -0
- package/packs/core/commands/next.md +36 -0
- package/packs/core/commands/restart.md +58 -0
- package/packs/core/commands/restore.md +29 -0
- package/packs/core/commands/start.md +57 -0
- package/packs/core/commands/stop.md +65 -0
- package/packs/core/commands/summary.md +40 -0
- package/packs/core/manifest.json +24 -0
- package/packs/ops/decision-recovery/instructions.md +65 -0
- package/packs/ops/decision-recovery/pack.yaml +89 -0
- package/templates/claude-hooks/doc-ingest.js +76 -0
- package/dist/src/cli/commands/team.js +0 -168
- package/dist/src/core/context/shared-context-layer.js +0 -620
- package/dist/src/core/context/stack-merge-resolver.js +0 -748
- package/dist/src/core/merge/conflict-detector.js +0 -430
- package/dist/src/core/merge/resolution-engine.js +0 -557
- package/dist/src/core/merge/stack-diff.js +0 -531
- package/dist/src/core/merge/unified-merge-resolver.js +0 -302
- package/dist/src/integrations/mcp/handlers/cord-handlers.js +0 -397
- package/dist/src/integrations/mcp/handlers/team-handlers.js +0 -211
- /package/dist/src/core/{merge → cache}/types.js +0 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import * as fs from "fs/promises";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import * as os from "os";
|
|
8
|
+
import { createHash, randomUUID } from "crypto";
|
|
9
|
+
function getBaseStateDir() {
|
|
10
|
+
const xdgState = process.env["XDG_STATE_HOME"]?.trim();
|
|
11
|
+
if (xdgState) {
|
|
12
|
+
return path.join(xdgState, "stackmemory");
|
|
13
|
+
}
|
|
14
|
+
const homeDir = process.env["HOME"] || process.env["USERPROFILE"] || os.homedir();
|
|
15
|
+
return path.join(homeDir, ".stackmemory");
|
|
16
|
+
}
|
|
17
|
+
function projectIdFromIdentifier(identifier) {
|
|
18
|
+
return identifier.replace(/\.git$/, "").replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase().slice(-50);
|
|
19
|
+
}
|
|
20
|
+
function normalizeProjectId(projectId, projectPath) {
|
|
21
|
+
if (projectId && projectId.trim()) {
|
|
22
|
+
return projectIdFromIdentifier(projectId.trim());
|
|
23
|
+
}
|
|
24
|
+
if (!projectPath || !projectPath.trim()) {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
return createHash("sha1").update(projectPath.trim().toLowerCase()).digest("hex").slice(0, 16);
|
|
28
|
+
}
|
|
29
|
+
async function pathExists(targetPath) {
|
|
30
|
+
try {
|
|
31
|
+
await fs.access(targetPath);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
class CanonicalStateStore {
|
|
38
|
+
rootDir;
|
|
39
|
+
constructor(rootDir = path.join(getBaseStateDir(), "shared-state")) {
|
|
40
|
+
this.rootDir = rootDir;
|
|
41
|
+
}
|
|
42
|
+
getRootDir() {
|
|
43
|
+
return this.rootDir;
|
|
44
|
+
}
|
|
45
|
+
async initialize() {
|
|
46
|
+
await fs.mkdir(this.getInstancesDir(), { recursive: true });
|
|
47
|
+
await fs.mkdir(this.getSessionsDir(), { recursive: true });
|
|
48
|
+
await fs.mkdir(this.getEventsDir(), { recursive: true });
|
|
49
|
+
await fs.mkdir(this.getGithubDir(), { recursive: true });
|
|
50
|
+
await fs.mkdir(this.getClaimsDir(), { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
async upsertInstance(input) {
|
|
53
|
+
await this.initialize();
|
|
54
|
+
const filePath = this.getInstanceFile(input.instanceId);
|
|
55
|
+
const existing = await this.readJsonFile(filePath);
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const record = {
|
|
58
|
+
instanceId: input.instanceId,
|
|
59
|
+
tool: input.tool,
|
|
60
|
+
sessionId: input.sessionId ?? existing?.sessionId,
|
|
61
|
+
projectId: normalizeProjectId(input.projectId, input.projectPath) ?? existing?.projectId,
|
|
62
|
+
projectPath: input.projectPath ?? existing?.projectPath,
|
|
63
|
+
branch: input.branch ?? existing?.branch,
|
|
64
|
+
worktreePath: input.worktreePath ?? existing?.worktreePath,
|
|
65
|
+
pid: input.pid ?? existing?.pid,
|
|
66
|
+
startedAt: existing?.startedAt ?? input.startedAt ?? now,
|
|
67
|
+
lastSeenAt: input.lastSeenAt ?? now,
|
|
68
|
+
status: input.status ?? existing?.status ?? "active",
|
|
69
|
+
metadata: {
|
|
70
|
+
...existing?.metadata || {},
|
|
71
|
+
...input.metadata || {}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
await this.writeJsonFile(filePath, record);
|
|
75
|
+
if (record.sessionId) {
|
|
76
|
+
await this.upsertSession({
|
|
77
|
+
sessionId: record.sessionId,
|
|
78
|
+
tool: record.tool,
|
|
79
|
+
projectId: record.projectId,
|
|
80
|
+
projectPath: record.projectPath,
|
|
81
|
+
branch: record.branch,
|
|
82
|
+
instanceId: record.instanceId,
|
|
83
|
+
metadata: record.metadata
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
async endInstance(instanceId) {
|
|
89
|
+
await this.initialize();
|
|
90
|
+
const filePath = this.getInstanceFile(instanceId);
|
|
91
|
+
const existing = await this.readJsonFile(filePath);
|
|
92
|
+
if (!existing) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const updated = {
|
|
96
|
+
...existing,
|
|
97
|
+
status: "ended",
|
|
98
|
+
lastSeenAt: Date.now()
|
|
99
|
+
};
|
|
100
|
+
await this.writeJsonFile(filePath, updated);
|
|
101
|
+
}
|
|
102
|
+
async upsertSession(input) {
|
|
103
|
+
await this.initialize();
|
|
104
|
+
const filePath = this.getSessionFile(input.sessionId);
|
|
105
|
+
const existing = await this.readJsonFile(filePath);
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
const nextInstanceIds = new Set(existing?.instanceIds || []);
|
|
108
|
+
if (input.instanceId) {
|
|
109
|
+
nextInstanceIds.add(input.instanceId);
|
|
110
|
+
}
|
|
111
|
+
for (const instanceId of input.instanceIds || []) {
|
|
112
|
+
nextInstanceIds.add(instanceId);
|
|
113
|
+
}
|
|
114
|
+
const record = {
|
|
115
|
+
sessionId: input.sessionId,
|
|
116
|
+
tool: input.tool,
|
|
117
|
+
projectId: normalizeProjectId(input.projectId, input.projectPath) ?? existing?.projectId,
|
|
118
|
+
projectPath: input.projectPath ?? existing?.projectPath,
|
|
119
|
+
branch: input.branch ?? existing?.branch,
|
|
120
|
+
startedAt: existing?.startedAt ?? input.startedAt ?? now,
|
|
121
|
+
lastSeenAt: input.lastSeenAt ?? now,
|
|
122
|
+
status: input.status ?? existing?.status ?? "active",
|
|
123
|
+
instanceIds: Array.from(nextInstanceIds),
|
|
124
|
+
metadata: {
|
|
125
|
+
...existing?.metadata || {},
|
|
126
|
+
...input.metadata || {}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
await this.writeJsonFile(filePath, record);
|
|
130
|
+
return record;
|
|
131
|
+
}
|
|
132
|
+
async endSession(sessionId, status = "closed") {
|
|
133
|
+
await this.initialize();
|
|
134
|
+
const filePath = this.getSessionFile(sessionId);
|
|
135
|
+
const existing = await this.readJsonFile(filePath);
|
|
136
|
+
if (!existing) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const updated = {
|
|
140
|
+
...existing,
|
|
141
|
+
status,
|
|
142
|
+
lastSeenAt: Date.now()
|
|
143
|
+
};
|
|
144
|
+
await this.writeJsonFile(filePath, updated);
|
|
145
|
+
}
|
|
146
|
+
async saveGitHubPullRequest(projection) {
|
|
147
|
+
await this.initialize();
|
|
148
|
+
const normalizedProjectId = normalizeProjectId(projection.projectId, projection.projectPath) ?? projection.projectId;
|
|
149
|
+
const filePath = this.getGitHubPullRequestFile(
|
|
150
|
+
projection.repo,
|
|
151
|
+
projection.branch
|
|
152
|
+
);
|
|
153
|
+
const record = {
|
|
154
|
+
...projection,
|
|
155
|
+
projectId: normalizedProjectId,
|
|
156
|
+
lastSyncedAt: projection.lastSyncedAt || Date.now()
|
|
157
|
+
};
|
|
158
|
+
await this.writeJsonFile(filePath, record);
|
|
159
|
+
return record;
|
|
160
|
+
}
|
|
161
|
+
async claimPaths(input) {
|
|
162
|
+
await this.initialize();
|
|
163
|
+
await this.cleanupExpiredClaims();
|
|
164
|
+
const now = input.lastSeenAt ?? Date.now();
|
|
165
|
+
const existing = input.claimId ? await this.readJsonFile(
|
|
166
|
+
this.getClaimFile(input.claimId)
|
|
167
|
+
) : null;
|
|
168
|
+
const record = {
|
|
169
|
+
claimId: input.claimId || randomUUID(),
|
|
170
|
+
tool: input.tool,
|
|
171
|
+
sessionId: input.sessionId ?? existing?.sessionId,
|
|
172
|
+
instanceId: input.instanceId ?? existing?.instanceId,
|
|
173
|
+
projectId: normalizeProjectId(input.projectId, input.projectPath) ?? existing?.projectId,
|
|
174
|
+
projectPath: input.projectPath ?? existing?.projectPath,
|
|
175
|
+
branch: input.branch ?? existing?.branch,
|
|
176
|
+
paths: Array.from(
|
|
177
|
+
new Set(
|
|
178
|
+
(input.paths ?? existing?.paths ?? []).map((item) => item.trim()).filter(Boolean)
|
|
179
|
+
)
|
|
180
|
+
),
|
|
181
|
+
status: input.status ?? "active",
|
|
182
|
+
claimedAt: existing?.claimedAt ?? now,
|
|
183
|
+
lastSeenAt: now,
|
|
184
|
+
expiresAt: input.expiresAt ?? now + Math.max(1, input.ttlMs ?? 24 * 60 * 60 * 1e3),
|
|
185
|
+
metadata: {
|
|
186
|
+
...existing?.metadata || {},
|
|
187
|
+
...input.metadata || {}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const conflicts = (await this.listPathClaims({
|
|
191
|
+
projectId: record.projectId,
|
|
192
|
+
projectPath: record.projectPath,
|
|
193
|
+
activeOnly: true
|
|
194
|
+
})).filter((claim) => claim.claimId !== record.claimId).filter((claim) => this.claimsOverlap(record, claim)).map((claim) => ({
|
|
195
|
+
claimId: claim.claimId,
|
|
196
|
+
branch: claim.branch,
|
|
197
|
+
paths: claim.paths,
|
|
198
|
+
sessionId: claim.sessionId,
|
|
199
|
+
instanceId: claim.instanceId
|
|
200
|
+
}));
|
|
201
|
+
await this.writeJsonFile(this.getClaimFile(record.claimId), record);
|
|
202
|
+
return { record, conflicts };
|
|
203
|
+
}
|
|
204
|
+
async releaseClaims(options) {
|
|
205
|
+
await this.initialize();
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
let released = 0;
|
|
208
|
+
const claims = await this.listPathClaims({
|
|
209
|
+
projectId: options.projectId,
|
|
210
|
+
projectPath: options.projectPath,
|
|
211
|
+
activeOnly: false
|
|
212
|
+
});
|
|
213
|
+
for (const claim of claims) {
|
|
214
|
+
if (claim.status !== "active") {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (options.claimId && claim.claimId !== options.claimId) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (options.instanceId && claim.instanceId !== options.instanceId) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (options.sessionId && claim.sessionId !== options.sessionId) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (options.branch && claim.branch !== options.branch) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
await this.writeJsonFile(this.getClaimFile(claim.claimId), {
|
|
230
|
+
...claim,
|
|
231
|
+
status: "released",
|
|
232
|
+
lastSeenAt: now,
|
|
233
|
+
releasedAt: now,
|
|
234
|
+
releaseReason: options.reason || claim.releaseReason
|
|
235
|
+
});
|
|
236
|
+
released++;
|
|
237
|
+
}
|
|
238
|
+
return released;
|
|
239
|
+
}
|
|
240
|
+
async listPathClaims(options) {
|
|
241
|
+
await this.initialize();
|
|
242
|
+
await this.cleanupExpiredClaims();
|
|
243
|
+
const projectId = normalizeProjectId(
|
|
244
|
+
options?.projectId,
|
|
245
|
+
options?.projectPath
|
|
246
|
+
);
|
|
247
|
+
const dir = this.getClaimsDir();
|
|
248
|
+
const entries = await fs.readdir(dir);
|
|
249
|
+
const claims = await Promise.all(
|
|
250
|
+
entries.filter((entry) => entry.endsWith(".json")).map(
|
|
251
|
+
(entry) => this.readJsonFile(path.join(dir, entry))
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
return claims.filter(Boolean).filter((claim) => !options?.activeOnly || claim.status === "active").filter(
|
|
255
|
+
(claim) => !projectId || this.matchesProject(
|
|
256
|
+
claim.projectId,
|
|
257
|
+
claim.projectPath,
|
|
258
|
+
projectId,
|
|
259
|
+
options?.projectPath
|
|
260
|
+
)
|
|
261
|
+
).sort((a, b) => b.lastSeenAt - a.lastSeenAt);
|
|
262
|
+
}
|
|
263
|
+
async listActiveProjectPaths() {
|
|
264
|
+
await this.initialize();
|
|
265
|
+
const projectPaths = /* @__PURE__ */ new Set();
|
|
266
|
+
const sessions = await this.listSessions();
|
|
267
|
+
for (const session of sessions) {
|
|
268
|
+
if (session.status === "active" && session.projectPath) {
|
|
269
|
+
projectPaths.add(session.projectPath);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const instances = await this.listInstances();
|
|
273
|
+
for (const instance of instances) {
|
|
274
|
+
if (instance.status === "active" && instance.projectPath) {
|
|
275
|
+
projectPaths.add(instance.projectPath);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const pullRequests = await this.listGitHubPullRequests();
|
|
279
|
+
for (const projection of pullRequests) {
|
|
280
|
+
if (projection.projectPath) {
|
|
281
|
+
projectPaths.add(projection.projectPath);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return Array.from(projectPaths).sort();
|
|
285
|
+
}
|
|
286
|
+
async getGitHubPullRequest(options) {
|
|
287
|
+
await this.initialize();
|
|
288
|
+
return this.readJsonFile(
|
|
289
|
+
this.getGitHubPullRequestFile(options.repo, options.branch)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
async listGitHubPullRequests(options) {
|
|
293
|
+
await this.initialize();
|
|
294
|
+
const projectId = normalizeProjectId(
|
|
295
|
+
options?.projectId,
|
|
296
|
+
options?.projectPath
|
|
297
|
+
);
|
|
298
|
+
const dir = this.getGithubDir();
|
|
299
|
+
const entries = await fs.readdir(dir);
|
|
300
|
+
const records = await Promise.all(
|
|
301
|
+
entries.filter((entry) => entry.endsWith(".json")).map(
|
|
302
|
+
(entry) => this.readJsonFile(path.join(dir, entry))
|
|
303
|
+
)
|
|
304
|
+
);
|
|
305
|
+
return records.filter(Boolean).filter(
|
|
306
|
+
(record) => !projectId || this.matchesProject(
|
|
307
|
+
record.projectId,
|
|
308
|
+
record.projectPath,
|
|
309
|
+
projectId,
|
|
310
|
+
options?.projectPath
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
async appendEvent(input) {
|
|
315
|
+
await this.initialize();
|
|
316
|
+
const event = {
|
|
317
|
+
id: input.id || randomUUID(),
|
|
318
|
+
timestamp: input.timestamp || Date.now(),
|
|
319
|
+
type: input.type,
|
|
320
|
+
tool: input.tool,
|
|
321
|
+
instanceId: input.instanceId,
|
|
322
|
+
sessionId: input.sessionId,
|
|
323
|
+
projectId: normalizeProjectId(input.projectId, input.projectPath),
|
|
324
|
+
projectPath: input.projectPath,
|
|
325
|
+
branch: input.branch,
|
|
326
|
+
payload: input.payload || {}
|
|
327
|
+
};
|
|
328
|
+
const date = new Date(event.timestamp).toISOString().slice(0, 10);
|
|
329
|
+
const eventFile = path.join(this.getEventsDir(), `${date}.jsonl`);
|
|
330
|
+
await fs.appendFile(eventFile, `${JSON.stringify(event)}
|
|
331
|
+
`, "utf8");
|
|
332
|
+
if (event.instanceId) {
|
|
333
|
+
const instance = await this.readJsonFile(
|
|
334
|
+
this.getInstanceFile(event.instanceId)
|
|
335
|
+
);
|
|
336
|
+
if (instance) {
|
|
337
|
+
await this.upsertInstance({
|
|
338
|
+
...instance,
|
|
339
|
+
lastSeenAt: event.timestamp
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (event.sessionId) {
|
|
344
|
+
const session = await this.readJsonFile(
|
|
345
|
+
this.getSessionFile(event.sessionId)
|
|
346
|
+
);
|
|
347
|
+
if (session) {
|
|
348
|
+
await this.upsertSession({
|
|
349
|
+
...session,
|
|
350
|
+
lastSeenAt: event.timestamp,
|
|
351
|
+
instanceIds: session.instanceIds
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return event;
|
|
356
|
+
}
|
|
357
|
+
async listSessions() {
|
|
358
|
+
await this.initialize();
|
|
359
|
+
const dir = this.getSessionsDir();
|
|
360
|
+
const entries = await fs.readdir(dir);
|
|
361
|
+
const sessions = await Promise.all(
|
|
362
|
+
entries.filter((entry) => entry.endsWith(".json")).map(
|
|
363
|
+
(entry) => this.readJsonFile(path.join(dir, entry))
|
|
364
|
+
)
|
|
365
|
+
);
|
|
366
|
+
return sessions.filter(Boolean);
|
|
367
|
+
}
|
|
368
|
+
async listInstances() {
|
|
369
|
+
await this.initialize();
|
|
370
|
+
const dir = this.getInstancesDir();
|
|
371
|
+
const entries = await fs.readdir(dir);
|
|
372
|
+
const instances = await Promise.all(
|
|
373
|
+
entries.filter((entry) => entry.endsWith(".json")).map(
|
|
374
|
+
(entry) => this.readJsonFile(path.join(dir, entry))
|
|
375
|
+
)
|
|
376
|
+
);
|
|
377
|
+
return instances.filter(Boolean);
|
|
378
|
+
}
|
|
379
|
+
async getProjectSummary(options) {
|
|
380
|
+
await this.initialize();
|
|
381
|
+
const projectId = normalizeProjectId(
|
|
382
|
+
options.projectId,
|
|
383
|
+
options.projectPath
|
|
384
|
+
);
|
|
385
|
+
const sessions = (await this.listSessions()).filter(
|
|
386
|
+
(session) => session.status === "active" && this.matchesProject(
|
|
387
|
+
session.projectId,
|
|
388
|
+
session.projectPath,
|
|
389
|
+
projectId,
|
|
390
|
+
options.projectPath
|
|
391
|
+
)
|
|
392
|
+
);
|
|
393
|
+
const instances = (await this.listInstances()).filter(
|
|
394
|
+
(instance) => instance.status === "active" && this.matchesProject(
|
|
395
|
+
instance.projectId,
|
|
396
|
+
instance.projectPath,
|
|
397
|
+
projectId,
|
|
398
|
+
options.projectPath
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
const activeClaims = await this.listPathClaims({
|
|
402
|
+
projectId,
|
|
403
|
+
projectPath: options.projectPath,
|
|
404
|
+
activeOnly: true
|
|
405
|
+
});
|
|
406
|
+
const recentEvents = await this.listRecentEvents({
|
|
407
|
+
projectId,
|
|
408
|
+
projectPath: options.projectPath,
|
|
409
|
+
limit: options.eventLimit || 10
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
projectId,
|
|
413
|
+
projectPath: options.projectPath,
|
|
414
|
+
activeSessions: sessions.sort((a, b) => b.lastSeenAt - a.lastSeenAt),
|
|
415
|
+
activeInstances: instances.sort((a, b) => b.lastSeenAt - a.lastSeenAt),
|
|
416
|
+
activeClaims,
|
|
417
|
+
recentEvents
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
async cleanupExpiredClaims() {
|
|
421
|
+
const dir = this.getClaimsDir();
|
|
422
|
+
const entries = await fs.readdir(dir).catch(() => []);
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
if (!entry.endsWith(".json")) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const filePath = path.join(dir, entry);
|
|
429
|
+
const claim = await this.readJsonFile(filePath);
|
|
430
|
+
if (!claim || claim.status !== "active" || claim.expiresAt > now) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
await this.writeJsonFile(filePath, {
|
|
434
|
+
...claim,
|
|
435
|
+
status: "expired",
|
|
436
|
+
releasedAt: now,
|
|
437
|
+
releaseReason: claim.releaseReason || "expired",
|
|
438
|
+
lastSeenAt: now
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
claimsOverlap(left, right) {
|
|
443
|
+
if (left.branch && right.branch && left.branch.trim() && right.branch.trim() && left.branch === right.branch) {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
for (const leftPath of left.paths) {
|
|
447
|
+
for (const rightPath of right.paths) {
|
|
448
|
+
if (this.pathsOverlap(leftPath, rightPath)) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
pathsOverlap(left, right) {
|
|
456
|
+
const normalizedLeft = this.normalizeClaimPath(left);
|
|
457
|
+
const normalizedRight = this.normalizeClaimPath(right);
|
|
458
|
+
if (!normalizedLeft || !normalizedRight) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
if (normalizedLeft === "*" || normalizedRight === "*" || normalizedLeft === "." || normalizedRight === ".") {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
if (normalizedLeft === normalizedRight) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
return normalizedLeft.startsWith(`${normalizedRight}/`) || normalizedRight.startsWith(`${normalizedLeft}/`);
|
|
468
|
+
}
|
|
469
|
+
normalizeClaimPath(value) {
|
|
470
|
+
return value.trim().replace(/\\/g, "/").replace(/\/\*\*$/, "").replace(/\/$/, "");
|
|
471
|
+
}
|
|
472
|
+
async listRecentEvents(options) {
|
|
473
|
+
await this.initialize();
|
|
474
|
+
const projectId = normalizeProjectId(
|
|
475
|
+
options.projectId,
|
|
476
|
+
options.projectPath
|
|
477
|
+
);
|
|
478
|
+
const eventDir = this.getEventsDir();
|
|
479
|
+
const eventFiles = (await fs.readdir(eventDir)).filter((entry) => entry.endsWith(".jsonl")).sort().reverse().slice(0, 7);
|
|
480
|
+
const events = [];
|
|
481
|
+
for (const entry of eventFiles) {
|
|
482
|
+
const filePath = path.join(eventDir, entry);
|
|
483
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
484
|
+
const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
485
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
486
|
+
try {
|
|
487
|
+
const parsed = JSON.parse(lines[i]);
|
|
488
|
+
if (this.matchesProject(
|
|
489
|
+
parsed.projectId,
|
|
490
|
+
parsed.projectPath,
|
|
491
|
+
projectId,
|
|
492
|
+
options.projectPath
|
|
493
|
+
)) {
|
|
494
|
+
events.push(parsed);
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
if (events.length >= (options.limit || 20)) {
|
|
499
|
+
return events;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return events;
|
|
504
|
+
}
|
|
505
|
+
matchesProject(candidateProjectId, candidateProjectPath, projectId, projectPath) {
|
|
506
|
+
if (projectId && candidateProjectId) {
|
|
507
|
+
return candidateProjectId === projectId;
|
|
508
|
+
}
|
|
509
|
+
if (projectPath && candidateProjectPath) {
|
|
510
|
+
return candidateProjectPath === projectPath;
|
|
511
|
+
}
|
|
512
|
+
return !projectId && !projectPath;
|
|
513
|
+
}
|
|
514
|
+
getInstancesDir() {
|
|
515
|
+
return path.join(this.rootDir, "instances");
|
|
516
|
+
}
|
|
517
|
+
getSessionsDir() {
|
|
518
|
+
return path.join(this.rootDir, "sessions");
|
|
519
|
+
}
|
|
520
|
+
getEventsDir() {
|
|
521
|
+
return path.join(this.rootDir, "events");
|
|
522
|
+
}
|
|
523
|
+
getGithubDir() {
|
|
524
|
+
return path.join(this.rootDir, "github", "pull-requests");
|
|
525
|
+
}
|
|
526
|
+
getClaimsDir() {
|
|
527
|
+
return path.join(this.rootDir, "claims");
|
|
528
|
+
}
|
|
529
|
+
getInstanceFile(instanceId) {
|
|
530
|
+
return path.join(this.getInstancesDir(), `${instanceId}.json`);
|
|
531
|
+
}
|
|
532
|
+
getSessionFile(sessionId) {
|
|
533
|
+
return path.join(this.getSessionsDir(), `${sessionId}.json`);
|
|
534
|
+
}
|
|
535
|
+
getGitHubPullRequestFile(repo, branch) {
|
|
536
|
+
const slug = `${repo}__${branch}`.replace(/[\\/]/g, "__").replace(/[^a-zA-Z0-9_.-]/g, "-");
|
|
537
|
+
return path.join(this.getGithubDir(), `${slug}.json`);
|
|
538
|
+
}
|
|
539
|
+
getClaimFile(claimId) {
|
|
540
|
+
return path.join(this.getClaimsDir(), `${claimId}.json`);
|
|
541
|
+
}
|
|
542
|
+
async readJsonFile(filePath) {
|
|
543
|
+
if (!await pathExists(filePath)) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
547
|
+
return JSON.parse(content);
|
|
548
|
+
}
|
|
549
|
+
async writeJsonFile(filePath, value) {
|
|
550
|
+
const dir = path.dirname(filePath);
|
|
551
|
+
await fs.mkdir(dir, { recursive: true });
|
|
552
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
553
|
+
await fs.writeFile(tempPath, JSON.stringify(value, null, 2));
|
|
554
|
+
await fs.rename(tempPath, filePath);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const canonicalStateStore = new CanonicalStateStore();
|
|
558
|
+
export {
|
|
559
|
+
CanonicalStateStore,
|
|
560
|
+
canonicalStateStore,
|
|
561
|
+
getBaseStateDir as getCanonicalStateBaseDir,
|
|
562
|
+
normalizeProjectId,
|
|
563
|
+
projectIdFromIdentifier
|
|
564
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
export * from "./types.js";
|
|
6
|
+
import { parsePackYaml, loadPackFromDir } from "./parser.js";
|
|
7
|
+
import {
|
|
8
|
+
SkillPackRegistry,
|
|
9
|
+
getSkillPackRegistry,
|
|
10
|
+
resetSkillPackRegistry
|
|
11
|
+
} from "./registry.js";
|
|
12
|
+
export {
|
|
13
|
+
SkillPackRegistry,
|
|
14
|
+
getSkillPackRegistry,
|
|
15
|
+
loadPackFromDir,
|
|
16
|
+
parsePackYaml,
|
|
17
|
+
resetSkillPackRegistry
|
|
18
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import * as yaml from "js-yaml";
|
|
8
|
+
import { SkillPackManifestSchema } from "./types.js";
|
|
9
|
+
import { logger } from "../monitoring/logger.js";
|
|
10
|
+
function parsePackYaml(content) {
|
|
11
|
+
const raw = yaml.load(content);
|
|
12
|
+
const manifest = SkillPackManifestSchema.parse(raw);
|
|
13
|
+
const instructions = manifest.instructions;
|
|
14
|
+
return { manifest, instructions };
|
|
15
|
+
}
|
|
16
|
+
async function loadPackFromDir(dir) {
|
|
17
|
+
const yamlPath = path.join(dir, "pack.yaml");
|
|
18
|
+
if (!fs.existsSync(yamlPath)) {
|
|
19
|
+
throw new Error(`pack.yaml not found in ${dir}`);
|
|
20
|
+
}
|
|
21
|
+
const content = fs.readFileSync(yamlPath, "utf-8");
|
|
22
|
+
const pack = parsePackYaml(content);
|
|
23
|
+
if (pack.manifest.instructions?.endsWith(".md")) {
|
|
24
|
+
const instrPath = path.join(dir, pack.manifest.instructions);
|
|
25
|
+
if (fs.existsSync(instrPath)) {
|
|
26
|
+
pack.instructions = fs.readFileSync(instrPath, "utf-8");
|
|
27
|
+
logger.debug(
|
|
28
|
+
`Loaded instructions from ${instrPath} for pack ${pack.manifest.name}`
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
logger.warn(
|
|
32
|
+
`Instructions file ${instrPath} referenced but not found for pack ${pack.manifest.name}`
|
|
33
|
+
);
|
|
34
|
+
pack.instructions = void 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return pack;
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
loadPackFromDir,
|
|
41
|
+
parsePackYaml
|
|
42
|
+
};
|