@stackmemoryai/stackmemory 1.10.5 → 1.14.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/LICENSE +131 -64
- package/README.md +107 -24
- package/bin/claude-sm +16 -1
- package/bin/claude-smd +16 -1
- package/bin/codex-smd +16 -1
- package/bin/gemini-sm +16 -1
- package/bin/hermes-sm +21 -0
- package/bin/hermes-smd +21 -0
- package/bin/opencode-sm +16 -1
- package/dist/src/cli/claude-sm.js +266 -84
- package/dist/src/cli/codex-sm.js +225 -33
- package/dist/src/cli/commands/bench.js +209 -2
- package/dist/src/cli/commands/brain.js +206 -0
- package/dist/src/cli/commands/cache.js +126 -0
- package/dist/src/cli/commands/company-os.js +184 -0
- package/dist/src/cli/commands/context.js +5 -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/operator.js +127 -0
- package/dist/src/cli/commands/optimize.js +117 -0
- package/dist/src/cli/commands/orchestrate.js +232 -5
- package/dist/src/cli/commands/orchestrator.js +315 -26
- package/dist/src/cli/commands/pack.js +322 -0
- package/dist/src/cli/commands/patterns.js +254 -0
- package/dist/src/cli/commands/portal.js +161 -0
- package/dist/src/cli/commands/scaffold.js +92 -0
- package/dist/src/cli/commands/search.js +40 -1
- package/dist/src/cli/commands/setup.js +178 -11
- package/dist/src/cli/commands/skills.js +10 -1
- package/dist/src/cli/commands/state.js +265 -0
- package/dist/src/cli/commands/sync.js +253 -0
- package/dist/src/cli/commands/tasks.js +130 -1
- package/dist/src/cli/commands/vision.js +221 -0
- package/dist/src/cli/gemini-sm.js +19 -29
- package/dist/src/cli/hermes-sm.js +224 -0
- package/dist/src/cli/index.js +105 -39
- 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 +116 -0
- package/dist/src/core/brain/brain-store.js +187 -0
- package/dist/src/core/brain/brain-sync.js +193 -0
- package/dist/src/core/brain/index.js +78 -0
- package/dist/src/core/brain/types.js +10 -0
- package/dist/src/core/cache/content-cache.js +238 -0
- package/dist/src/{integrations/diffmem → core/cache}/index.js +5 -5
- package/dist/src/core/cache/token-estimator.js +39 -0
- package/dist/src/core/config/feature-flags.js +2 -6
- package/dist/src/core/context/frame-database.js +79 -27
- package/dist/src/core/context/recursive-context-manager.js +1 -1
- package/dist/src/core/context/rehydration.js +2 -1
- package/dist/src/core/cross-search/cross-project-search.js +269 -0
- package/dist/src/core/cross-search/index.js +10 -0
- package/dist/src/core/database/sqlite-adapter.js +14 -84
- package/dist/src/core/extensions/provider-adapter.js +5 -0
- package/dist/src/core/models/model-router.js +54 -2
- package/dist/src/core/models/provider-pricing.js +58 -4
- package/dist/src/core/monitoring/logger.js +2 -1
- package/dist/src/core/optimization/trace-optimizer.js +413 -0
- package/dist/src/core/patterns/index.js +22 -0
- package/dist/src/core/patterns/pattern-applier.js +39 -0
- package/dist/src/core/patterns/pattern-observer.js +157 -0
- package/dist/src/core/patterns/pattern-store.js +259 -0
- package/dist/src/core/patterns/types.js +19 -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/retrieval/llm-context-retrieval.js +5 -4
- package/dist/src/core/retrieval/unified-context-assembler.js +11 -66
- 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 +79 -0
- package/dist/src/core/storage/cloud-sync-manager.js +116 -0
- package/dist/src/core/storage/cloud-sync.js +574 -0
- package/dist/src/core/storage/two-tier-storage.js +5 -1
- package/dist/src/core/tasks/master-tasks-template.js +43 -0
- package/dist/src/core/tasks/md-task-parser.js +138 -0
- package/dist/src/core/trace/trace-event-store.js +282 -0
- package/dist/src/core/vision/index.js +27 -0
- package/dist/src/core/vision/signals.js +79 -0
- package/dist/src/core/vision/types.js +22 -0
- package/dist/src/core/vision/vision-file.js +146 -0
- package/dist/src/core/vision/vision-loop.js +220 -0
- package/dist/src/core/wiki/wiki-compiler.js +103 -1
- package/dist/src/daemon/daemon-config.js +52 -0
- package/dist/src/daemon/services/desire-path-service.js +566 -0
- package/dist/src/daemon/services/github-service.js +126 -0
- package/dist/src/daemon/services/research-stream-service.js +320 -0
- package/dist/src/daemon/services/telemetry-service.js +192 -0
- package/dist/src/daemon/unified-daemon.js +58 -1
- package/dist/src/features/browser/cli-browser-agent.js +417 -0
- package/dist/src/features/browser/stagehand-workflows.js +578 -0
- package/dist/src/features/operator/adapter-factory.js +62 -0
- package/dist/src/features/operator/browser-adapter.js +109 -0
- package/dist/src/features/operator/desktop-adapter.js +125 -0
- package/dist/src/features/operator/index.js +39 -0
- package/dist/src/features/operator/llm-decision.js +137 -0
- package/dist/src/features/operator/operator-logger.js +92 -0
- package/dist/src/features/operator/overnight-runner.js +327 -0
- package/dist/src/features/operator/screen-adapter.js +91 -0
- package/dist/src/features/operator/session-manager.js +127 -0
- package/dist/src/features/operator/state-machine.js +227 -0
- package/dist/src/features/operator/task-queue.js +81 -0
- package/dist/src/features/portal/index.js +26 -0
- package/dist/src/features/portal/server.js +240 -0
- package/dist/src/features/portal/types.js +14 -0
- package/dist/src/features/portal/ui.js +195 -0
- package/dist/src/features/sweep/pty-wrapper.js +13 -5
- package/dist/src/features/tasks/task-aware-context.js +2 -1
- package/dist/src/features/tui/simple-monitor.js +0 -23
- package/dist/src/features/tui/swarm-monitor.js +8 -66
- package/dist/src/features/web/client/hooks/use-socket.js +12 -0
- package/dist/src/{core/merge/index.js → features/web/client/lib/utils.js} +8 -4
- package/dist/src/features/web/client/next-env.d.js +4 -0
- package/dist/src/features/web/client/stores/session-store.js +12 -0
- package/dist/src/features/web/server/gcp-billing.js +76 -0
- package/dist/src/features/web/server/index.js +10 -0
- package/dist/src/features/web/server/spend-calculator.js +228 -0
- package/dist/src/hooks/schemas.js +6 -1
- package/dist/src/integrations/anthropic/client.js +3 -2
- package/dist/src/integrations/claude-code/agent-bridge.js +0 -3
- package/dist/src/integrations/claude-code/subagent-client.js +307 -11
- package/dist/src/integrations/claude-code/task-coordinator.js +2 -1
- package/dist/src/integrations/github/pr-state.js +158 -0
- package/dist/src/integrations/linear/client.js +4 -1
- package/dist/src/integrations/linear/webhook-retry.js +196 -0
- package/dist/src/integrations/linear/webhook-server.js +18 -22
- package/dist/src/integrations/mcp/handlers/cloud-sync-handlers.js +101 -0
- package/dist/src/integrations/mcp/handlers/index.js +40 -84
- package/dist/src/integrations/mcp/server.js +542 -641
- package/dist/src/integrations/mcp/tool-alias-registry.js +297 -0
- package/dist/src/integrations/mcp/tool-definitions.js +152 -682
- package/dist/src/mcp/stackmemory-mcp-server.js +571 -231
- package/dist/src/orchestrators/multimodal/determinism.js +244 -0
- package/dist/src/orchestrators/multimodal/harness.js +149 -78
- package/dist/src/orchestrators/multimodal/providers.js +44 -3
- package/dist/src/skills/recursive-agent-orchestrator.js +2 -4
- package/dist/src/utils/hook-installer.js +0 -8
- package/dist/src/utils/process-cleanup.js +1 -7
- package/docs/README.md +42 -0
- package/docs/guides/README_INSTALL.md +208 -0
- package/package.json +27 -9
- 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/scripts/claude-code-wrapper.sh +11 -0
- package/scripts/claude-sm-setup.sh +12 -1
- package/scripts/codex-wrapper.sh +11 -0
- package/scripts/git-hooks/branch-context-manager.sh +11 -0
- package/scripts/git-hooks/post-checkout-stackmemory.sh +11 -0
- package/scripts/git-hooks/post-commit-stackmemory.sh +11 -0
- package/scripts/git-hooks/pre-commit-stackmemory.sh +11 -0
- package/scripts/hooks/cleanup-shell.sh +12 -1
- package/scripts/hooks/task-complete.sh +12 -1
- package/scripts/install-code-execution-hooks.sh +12 -1
- package/scripts/install-sweep-hook.sh +12 -0
- package/scripts/install.sh +11 -0
- package/scripts/opencode-wrapper.sh +11 -0
- package/scripts/portal/cloud-init.yaml +69 -0
- package/scripts/portal/setup.sh +69 -0
- package/scripts/portal/stackmemory-portal.service +34 -0
- package/scripts/setup-claude-integration.sh +12 -1
- package/scripts/smoke-init-db.sh +23 -0
- package/scripts/stackmemory-daemon.sh +11 -0
- package/scripts/verify-dist.cjs +11 -4
- package/dist/src/cli/commands/ralph.js +0 -1053
- 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/hooks/diffmem-hooks.js +0 -376
- package/dist/src/integrations/diffmem/client.js +0 -208
- package/dist/src/integrations/diffmem/config.js +0 -14
- package/dist/src/integrations/greptile/client.js +0 -101
- package/dist/src/integrations/greptile/config.js +0 -14
- package/dist/src/integrations/greptile/index.js +0 -11
- package/dist/src/integrations/mcp/handlers/cord-handlers.js +0 -397
- package/dist/src/integrations/mcp/handlers/diffmem-handlers.js +0 -455
- package/dist/src/integrations/mcp/handlers/greptile-handlers.js +0 -456
- package/dist/src/integrations/mcp/handlers/provider-handlers.js +0 -227
- package/dist/src/integrations/mcp/handlers/team-handlers.js +0 -211
- package/dist/src/integrations/ralph/bridge/ralph-stackmemory-bridge.js +0 -863
- package/dist/src/integrations/ralph/context/context-budget-manager.js +0 -308
- package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +0 -391
- package/dist/src/integrations/ralph/index.js +0 -17
- package/dist/src/integrations/ralph/learning/pattern-learner.js +0 -435
- package/dist/src/integrations/ralph/lifecycle/iteration-lifecycle.js +0 -448
- package/dist/src/integrations/ralph/loopmax.js +0 -488
- package/dist/src/integrations/ralph/monitoring/swarm-dashboard.js +0 -293
- package/dist/src/integrations/ralph/monitoring/swarm-registry.js +0 -107
- package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +0 -508
- package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +0 -407
- package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +0 -495
- package/dist/src/integrations/ralph/patterns/oracle-worker-pattern.js +0 -387
- package/dist/src/integrations/ralph/performance/performance-optimizer.js +0 -357
- package/dist/src/integrations/ralph/recovery/crash-recovery.js +0 -461
- package/dist/src/integrations/ralph/state/state-reconciler.js +0 -420
- package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +0 -444
- package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -1007
- package/dist/src/integrations/ralph/visualization/ralph-debugger.js +0 -635
- package/scripts/ralph-loop-implementation.js +0 -404
- /package/dist/src/core/{merge → cache}/types.js +0 -0
- /package/dist/src/{integrations/diffmem/types.js → core/storage/cloud-sync-types.js} +0 -0
- /package/dist/src/{integrations/greptile/types.js → core/trace/trace-event.js} +0 -0
- /package/dist/src/{integrations/ralph → features/operator}/types.js +0 -0
|
@@ -0,0 +1,566 @@
|
|
|
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 {
|
|
6
|
+
existsSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
appendFileSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
statSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
unlinkSync
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { join, basename, dirname, extname } from "path";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { randomUUID } from "crypto";
|
|
19
|
+
const SM_DIR = join(homedir(), ".stackmemory");
|
|
20
|
+
const DP_DIR = join(SM_DIR, "desire-paths");
|
|
21
|
+
const STREAM_FILE = join(DP_DIR, "action-stream.jsonl");
|
|
22
|
+
const PATTERNS_FILE = join(DP_DIR, "patterns.json");
|
|
23
|
+
const SUGGESTIONS_DIR = join(DP_DIR, "suggestions");
|
|
24
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
25
|
+
const TOOL_TARGET_SENSITIVE = /* @__PURE__ */ new Set(["Bash"]);
|
|
26
|
+
function sanitizePath(filePath) {
|
|
27
|
+
if (!filePath) return "*";
|
|
28
|
+
const dir = dirname(filePath);
|
|
29
|
+
const ext = extname(filePath);
|
|
30
|
+
if (ext) {
|
|
31
|
+
return `${dir}/*${ext}`;
|
|
32
|
+
}
|
|
33
|
+
return `${dir}/*`;
|
|
34
|
+
}
|
|
35
|
+
function sanitizeCommand(cmd) {
|
|
36
|
+
if (!cmd) return "*";
|
|
37
|
+
const parts = cmd.trim().split(/\s+/);
|
|
38
|
+
const command = parts[0];
|
|
39
|
+
const firstArg = parts.slice(1).find((p) => !p.startsWith("-"));
|
|
40
|
+
if (firstArg) {
|
|
41
|
+
return `${command} ${firstArg.length > 30 ? firstArg.slice(0, 30) + "*" : firstArg}`;
|
|
42
|
+
}
|
|
43
|
+
return command;
|
|
44
|
+
}
|
|
45
|
+
function actionKey(entry) {
|
|
46
|
+
return `${entry.tool}:${entry.target}`;
|
|
47
|
+
}
|
|
48
|
+
function sequenceHash(seq) {
|
|
49
|
+
return seq.join("|");
|
|
50
|
+
}
|
|
51
|
+
class DaemonDesirePathService {
|
|
52
|
+
config;
|
|
53
|
+
state;
|
|
54
|
+
scanTimeout;
|
|
55
|
+
isRunning = false;
|
|
56
|
+
onLog;
|
|
57
|
+
lastActivityTime = 0;
|
|
58
|
+
// last time an action was logged
|
|
59
|
+
consecutiveIdleScans = 0;
|
|
60
|
+
// scans with no new actions
|
|
61
|
+
constructor(config, onLog) {
|
|
62
|
+
this.config = config;
|
|
63
|
+
this.onLog = onLog;
|
|
64
|
+
this.state = {
|
|
65
|
+
lastScanTime: 0,
|
|
66
|
+
actionsLogged: 0,
|
|
67
|
+
patternsDetected: 0,
|
|
68
|
+
suggestionsGenerated: 0,
|
|
69
|
+
skillsAutoPromoted: 0,
|
|
70
|
+
errors: []
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
isOptedOut() {
|
|
74
|
+
if (process.env.STACKMEMORY_DESIRE_PATHS === "0" || process.env.STACKMEMORY_DESIRE_PATHS === "false") {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return !this.config.enabled;
|
|
78
|
+
}
|
|
79
|
+
// ─── 1. Action Stream Logger ─────────────────────────────
|
|
80
|
+
/** Append a tool call to the action stream. Called from hook events. */
|
|
81
|
+
logAction(entry) {
|
|
82
|
+
if (this.isOptedOut()) return;
|
|
83
|
+
try {
|
|
84
|
+
mkdirSync(DP_DIR, { recursive: true });
|
|
85
|
+
if (existsSync(STREAM_FILE)) {
|
|
86
|
+
const stat = statSync(STREAM_FILE);
|
|
87
|
+
if (stat.size > (this.config.maxLogSizeBytes || MAX_LOG_SIZE)) {
|
|
88
|
+
const rotated = `${STREAM_FILE}.${Date.now()}.bak`;
|
|
89
|
+
renameSync(STREAM_FILE, rotated);
|
|
90
|
+
this.onLog("INFO", "Action stream rotated", { size: stat.size });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
appendFileSync(STREAM_FILE, JSON.stringify(entry) + "\n", "utf-8");
|
|
94
|
+
this.state.actionsLogged++;
|
|
95
|
+
this.lastActivityTime = Date.now();
|
|
96
|
+
} catch (err) {
|
|
97
|
+
this.addError(String(err));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Parse a hook event into an ActionEntry. */
|
|
101
|
+
static parseHookEvent(toolName, firstArg, sessionId, durationMs) {
|
|
102
|
+
let target;
|
|
103
|
+
if (TOOL_TARGET_SENSITIVE.has(toolName)) {
|
|
104
|
+
target = sanitizeCommand(firstArg);
|
|
105
|
+
} else if (firstArg && (firstArg.includes("/") || firstArg.includes("\\"))) {
|
|
106
|
+
target = sanitizePath(firstArg);
|
|
107
|
+
} else {
|
|
108
|
+
target = firstArg ? firstArg.slice(0, 50) : "*";
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
112
|
+
sid: sessionId,
|
|
113
|
+
tool: toolName,
|
|
114
|
+
target,
|
|
115
|
+
dur: durationMs
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// ─── 2. Pattern Detector ──────────────────────────────────
|
|
119
|
+
/** Scan the action stream for repeated sequences. */
|
|
120
|
+
detectPatterns() {
|
|
121
|
+
if (!existsSync(STREAM_FILE)) return [];
|
|
122
|
+
let entries;
|
|
123
|
+
try {
|
|
124
|
+
const lines = readFileSync(STREAM_FILE, "utf-8").trim().split("\n");
|
|
125
|
+
entries = lines.map((line) => {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(line);
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}).filter(Boolean);
|
|
132
|
+
} catch {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
if (entries.length > 1e4) entries = entries.slice(-1e4);
|
|
136
|
+
if (entries.length < 3) return [];
|
|
137
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const sid = entry.sid || "unknown";
|
|
140
|
+
if (!sessions.has(sid)) sessions.set(sid, []);
|
|
141
|
+
sessions.get(sid).push(entry);
|
|
142
|
+
}
|
|
143
|
+
const maxLen = this.config.maxSequenceLength || 8;
|
|
144
|
+
const minLen = 2;
|
|
145
|
+
const sequenceCounts = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const [sid, actions] of sessions) {
|
|
147
|
+
const keys = actions.map(actionKey);
|
|
148
|
+
for (let len = minLen; len <= Math.min(maxLen, keys.length); len++) {
|
|
149
|
+
for (let i = 0; i <= keys.length - len; i++) {
|
|
150
|
+
const subseq = keys.slice(i, i + len);
|
|
151
|
+
const hash = sequenceHash(subseq);
|
|
152
|
+
if (!sequenceCounts.has(hash)) {
|
|
153
|
+
sequenceCounts.set(hash, {
|
|
154
|
+
count: 0,
|
|
155
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
156
|
+
firstSeen: actions[i].ts,
|
|
157
|
+
lastSeen: actions[i + len - 1].ts
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const entry = sequenceCounts.get(hash);
|
|
161
|
+
entry.count++;
|
|
162
|
+
entry.sessions.add(sid);
|
|
163
|
+
if (actions[i + len - 1].ts > entry.lastSeen) {
|
|
164
|
+
entry.lastSeen = actions[i + len - 1].ts;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const minFreq = this.config.minFrequency || 3;
|
|
170
|
+
const minSess = this.config.minSessions || 2;
|
|
171
|
+
const patterns = [];
|
|
172
|
+
for (const [hash, data] of sequenceCounts) {
|
|
173
|
+
if (data.count >= minFreq && data.sessions.size >= minSess) {
|
|
174
|
+
const sequence = hash.split("|");
|
|
175
|
+
patterns.push({
|
|
176
|
+
id: randomUUID().slice(0, 8),
|
|
177
|
+
sequence,
|
|
178
|
+
frequency: data.count,
|
|
179
|
+
sessions: data.sessions.size,
|
|
180
|
+
avg_steps: sequence.length,
|
|
181
|
+
first_seen: data.firstSeen,
|
|
182
|
+
last_seen: data.lastSeen,
|
|
183
|
+
score: data.count * data.sessions.size
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
patterns.sort((a, b) => b.score - a.score);
|
|
188
|
+
const filtered = [];
|
|
189
|
+
const seenHashes = /* @__PURE__ */ new Set();
|
|
190
|
+
for (const pattern of patterns) {
|
|
191
|
+
const hash = sequenceHash(pattern.sequence);
|
|
192
|
+
let isSubseq = false;
|
|
193
|
+
for (const accepted of filtered) {
|
|
194
|
+
const acceptedHash = sequenceHash(accepted.sequence);
|
|
195
|
+
if (acceptedHash.includes(hash) && acceptedHash !== hash) {
|
|
196
|
+
isSubseq = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!isSubseq && !seenHashes.has(hash)) {
|
|
201
|
+
filtered.push(pattern);
|
|
202
|
+
seenHashes.add(hash);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const topPatterns = filtered.slice(0, 20);
|
|
206
|
+
this.state.patternsDetected = topPatterns.length;
|
|
207
|
+
try {
|
|
208
|
+
writeFileSync(
|
|
209
|
+
PATTERNS_FILE,
|
|
210
|
+
JSON.stringify(
|
|
211
|
+
{ patterns: topPatterns, updated_at: (/* @__PURE__ */ new Date()).toISOString() },
|
|
212
|
+
null,
|
|
213
|
+
2
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
this.addError(String(err));
|
|
218
|
+
}
|
|
219
|
+
return topPatterns;
|
|
220
|
+
}
|
|
221
|
+
/** Load previously detected patterns. */
|
|
222
|
+
loadPatterns() {
|
|
223
|
+
try {
|
|
224
|
+
if (!existsSync(PATTERNS_FILE)) return [];
|
|
225
|
+
const data = JSON.parse(readFileSync(PATTERNS_FILE, "utf-8"));
|
|
226
|
+
return data.patterns || [];
|
|
227
|
+
} catch {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// ─── 3. Skill Suggester ───────────────────────────────────
|
|
232
|
+
/** Generate skill suggestions from detected patterns. */
|
|
233
|
+
generateSuggestions(patterns) {
|
|
234
|
+
const pats = patterns || this.loadPatterns();
|
|
235
|
+
if (pats.length === 0) return [];
|
|
236
|
+
mkdirSync(SUGGESTIONS_DIR, { recursive: true });
|
|
237
|
+
const suggestions = [];
|
|
238
|
+
for (const pattern of pats.slice(0, 10)) {
|
|
239
|
+
const uniqueTools = new Set(
|
|
240
|
+
pattern.sequence.map((s) => s.split(":", 2)[0].toLowerCase())
|
|
241
|
+
);
|
|
242
|
+
if (uniqueTools.size < 2 || pattern.sequence.length < 3) {
|
|
243
|
+
this.cleanTrivialSuggestion(pattern);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const suggestion = this.patternToSuggestion(pattern);
|
|
247
|
+
if (!suggestion) continue;
|
|
248
|
+
suggestions.push(suggestion);
|
|
249
|
+
const fileName = `${suggestion.name}.skill.md`;
|
|
250
|
+
const content = this.renderSkillMarkdown(suggestion);
|
|
251
|
+
try {
|
|
252
|
+
writeFileSync(join(SUGGESTIONS_DIR, fileName), content, "utf-8");
|
|
253
|
+
} catch (err) {
|
|
254
|
+
this.addError(String(err));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
this.state.suggestionsGenerated = suggestions.length;
|
|
258
|
+
this.autoPromote(suggestions);
|
|
259
|
+
return suggestions;
|
|
260
|
+
}
|
|
261
|
+
/** Remove suggestion files for trivial patterns that don't warrant skills. */
|
|
262
|
+
cleanTrivialSuggestion(pattern) {
|
|
263
|
+
const tools = pattern.sequence.map((s) => {
|
|
264
|
+
const [tool, target] = s.split(":", 2);
|
|
265
|
+
return { tool, target: target || "*" };
|
|
266
|
+
});
|
|
267
|
+
const toolNames = [...new Set(tools.map((t) => t.tool.toLowerCase()))];
|
|
268
|
+
const targets = tools.map((t) => t.target).filter((t) => t !== "*");
|
|
269
|
+
const dominantDir = targets.length > 0 ? targets[0].split("/").slice(0, 3).join("-").replace(/[^a-zA-Z0-9-]/g, "") : "";
|
|
270
|
+
const nameSuffix = dominantDir ? `-${dominantDir}` : "";
|
|
271
|
+
const name = `auto-${toolNames.join("-")}${nameSuffix}`;
|
|
272
|
+
const sugFile = join(SUGGESTIONS_DIR, `${name}.skill.md`);
|
|
273
|
+
try {
|
|
274
|
+
if (existsSync(sugFile)) {
|
|
275
|
+
unlinkSync(sugFile);
|
|
276
|
+
this.onLog("DEBUG", `Cleaned trivial suggestion: ${name}`, {
|
|
277
|
+
pattern_id: pattern.id,
|
|
278
|
+
uniqueTools: toolNames.length,
|
|
279
|
+
steps: pattern.sequence.length
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// ─── 4. Auto-Promotion ────────────────────────────────────
|
|
286
|
+
/**
|
|
287
|
+
* Auto-promote skills above confidence threshold.
|
|
288
|
+
* Copies from suggestions/ to the project's skills/ directory.
|
|
289
|
+
* Only promotes if: confidence ≥ threshold AND sessions ≥ minSessions.
|
|
290
|
+
*/
|
|
291
|
+
autoPromote(suggestions) {
|
|
292
|
+
const threshold = this.config.autoPromoteThreshold ?? 0.8;
|
|
293
|
+
const minSessions = this.config.autoPromoteMinSessions ?? 5;
|
|
294
|
+
if (threshold >= 1) return;
|
|
295
|
+
const skillsDir = this.config.skillsDir || this.findSkillsDir();
|
|
296
|
+
if (!skillsDir) return;
|
|
297
|
+
const patterns = this.loadPatterns();
|
|
298
|
+
for (const suggestion of suggestions) {
|
|
299
|
+
if (suggestion.confidence < threshold) continue;
|
|
300
|
+
const pattern = patterns.find((p) => p.id === suggestion.pattern_id);
|
|
301
|
+
if (!pattern || pattern.sessions < minSessions) continue;
|
|
302
|
+
const destFile = join(skillsDir, `${suggestion.name}.skill.md`);
|
|
303
|
+
if (existsSync(destFile)) continue;
|
|
304
|
+
const srcFile = join(SUGGESTIONS_DIR, `${suggestion.name}.skill.md`);
|
|
305
|
+
if (!existsSync(srcFile)) continue;
|
|
306
|
+
try {
|
|
307
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
308
|
+
let content = readFileSync(srcFile, "utf-8");
|
|
309
|
+
content = content.replace("status: suggested", "status: auto-promoted");
|
|
310
|
+
writeFileSync(destFile, content, "utf-8");
|
|
311
|
+
this.state.skillsAutoPromoted++;
|
|
312
|
+
this.onLog("INFO", `Skill auto-promoted: ${suggestion.name}`, {
|
|
313
|
+
confidence: suggestion.confidence,
|
|
314
|
+
sessions: pattern.sessions,
|
|
315
|
+
frequency: pattern.frequency,
|
|
316
|
+
dest: destFile
|
|
317
|
+
});
|
|
318
|
+
} catch (err) {
|
|
319
|
+
this.addError(
|
|
320
|
+
`Auto-promote failed for ${suggestion.name}: ${String(err)}`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** Find the best skills directory for auto-promotion. */
|
|
326
|
+
findSkillsDir() {
|
|
327
|
+
const cwd = process.cwd();
|
|
328
|
+
const candidates = [
|
|
329
|
+
join(cwd, ".claude", "skills", "knowledge"),
|
|
330
|
+
join(cwd, "skills")
|
|
331
|
+
];
|
|
332
|
+
for (const dir of candidates) {
|
|
333
|
+
if (existsSync(dir)) return dir;
|
|
334
|
+
}
|
|
335
|
+
const claudeDir = join(cwd, ".claude");
|
|
336
|
+
if (existsSync(claudeDir)) {
|
|
337
|
+
const target = join(claudeDir, "skills", "knowledge");
|
|
338
|
+
try {
|
|
339
|
+
mkdirSync(target, { recursive: true });
|
|
340
|
+
return target;
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
patternToSuggestion(pattern) {
|
|
348
|
+
if (pattern.sequence.length < 2) return null;
|
|
349
|
+
const tools = pattern.sequence.map((s) => {
|
|
350
|
+
const [tool, target] = s.split(":", 2);
|
|
351
|
+
return { tool, target: target || "*" };
|
|
352
|
+
});
|
|
353
|
+
const toolNames = [...new Set(tools.map((t) => t.tool.toLowerCase()))];
|
|
354
|
+
const targets = tools.map((t) => t.target).filter((t) => t !== "*");
|
|
355
|
+
const dominantDir = targets.length > 0 ? targets[0].split("/").slice(0, 3).join("-").replace(/[^a-zA-Z0-9-]/g, "") : "";
|
|
356
|
+
const nameSuffix = dominantDir ? `-${dominantDir}` : "";
|
|
357
|
+
const name = `auto-${toolNames.join("-")}${nameSuffix}`;
|
|
358
|
+
const firstTarget = tools[0].target;
|
|
359
|
+
const inputs = [];
|
|
360
|
+
if (firstTarget && firstTarget !== "*") {
|
|
361
|
+
inputs.push({
|
|
362
|
+
name: "target_path",
|
|
363
|
+
type: "string",
|
|
364
|
+
required: true,
|
|
365
|
+
description: `Path pattern (observed: ${firstTarget})`
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const lastTool = tools[tools.length - 1];
|
|
369
|
+
const outputs = [
|
|
370
|
+
{
|
|
371
|
+
name: "result",
|
|
372
|
+
type: "string",
|
|
373
|
+
description: `Output from ${lastTool.tool}`
|
|
374
|
+
}
|
|
375
|
+
];
|
|
376
|
+
const steps = tools.map((t, i) => `${i + 1}. ${t.tool}: ${t.target}`);
|
|
377
|
+
const confidence = Math.min(1, pattern.score / 20);
|
|
378
|
+
return {
|
|
379
|
+
name,
|
|
380
|
+
description: `Auto-detected workflow: ${toolNames.join(" \u2192 ")} (seen ${pattern.frequency}\xD7 across ${pattern.sessions} sessions)`,
|
|
381
|
+
inputs,
|
|
382
|
+
outputs,
|
|
383
|
+
steps,
|
|
384
|
+
pattern_id: pattern.id,
|
|
385
|
+
confidence,
|
|
386
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
renderSkillMarkdown(suggestion) {
|
|
390
|
+
const inputsYaml = suggestion.inputs.length > 0 ? suggestion.inputs.map(
|
|
391
|
+
(i) => ` - name: ${i.name}
|
|
392
|
+
type: ${i.type}
|
|
393
|
+
required: ${i.required}
|
|
394
|
+
description: "${i.description}"`
|
|
395
|
+
).join("\n") : "";
|
|
396
|
+
const outputsYaml = suggestion.outputs.map(
|
|
397
|
+
(o) => ` - name: ${o.name}
|
|
398
|
+
type: ${o.type}
|
|
399
|
+
description: "${o.description}"`
|
|
400
|
+
).join("\n");
|
|
401
|
+
return [
|
|
402
|
+
"---",
|
|
403
|
+
`name: ${suggestion.name}`,
|
|
404
|
+
`description: "${suggestion.description}"`,
|
|
405
|
+
`status: suggested`,
|
|
406
|
+
`pattern_id: ${suggestion.pattern_id}`,
|
|
407
|
+
`confidence: ${suggestion.confidence.toFixed(2)}`,
|
|
408
|
+
`generated_at: ${suggestion.generated_at}`,
|
|
409
|
+
suggestion.inputs.length > 0 ? `inputs:
|
|
410
|
+
${inputsYaml}` : "",
|
|
411
|
+
`outputs:
|
|
412
|
+
${outputsYaml}`,
|
|
413
|
+
"---",
|
|
414
|
+
"",
|
|
415
|
+
`# ${suggestion.name}`,
|
|
416
|
+
"",
|
|
417
|
+
"## Auto-Detected Workflow",
|
|
418
|
+
"",
|
|
419
|
+
`> This skill was auto-generated from ${suggestion.pattern_id} detected patterns.`,
|
|
420
|
+
"> Review and edit before promoting to an active skill.",
|
|
421
|
+
"",
|
|
422
|
+
"## Steps",
|
|
423
|
+
"",
|
|
424
|
+
...suggestion.steps,
|
|
425
|
+
"",
|
|
426
|
+
"## Notes",
|
|
427
|
+
"",
|
|
428
|
+
"- Edit this file to refine the workflow",
|
|
429
|
+
"- Move to your `skills/` directory to activate",
|
|
430
|
+
`- Confidence: ${(suggestion.confidence * 100).toFixed(0)}%`
|
|
431
|
+
].filter((line) => line !== "").join("\n") + "\n";
|
|
432
|
+
}
|
|
433
|
+
// ─── Lifecycle (adaptive backoff) ──────────────────────────
|
|
434
|
+
//
|
|
435
|
+
// Active sessions: scan every 1 hour
|
|
436
|
+
// Idle (no actions): backoff 1h → 2h → 4h → 8h → 12h (cap)
|
|
437
|
+
// New activity resets to 1h immediately
|
|
438
|
+
static BASE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
439
|
+
// 1 hour
|
|
440
|
+
static MAX_INTERVAL_MS = 12 * 60 * 60 * 1e3;
|
|
441
|
+
// 12 hours
|
|
442
|
+
static IDLE_THRESHOLD_MS = 30 * 60 * 1e3;
|
|
443
|
+
// 30 min = idle
|
|
444
|
+
getNextInterval() {
|
|
445
|
+
const now = Date.now();
|
|
446
|
+
const timeSinceActivity = now - this.lastActivityTime;
|
|
447
|
+
if (this.lastActivityTime > 0 && timeSinceActivity < DaemonDesirePathService.IDLE_THRESHOLD_MS) {
|
|
448
|
+
this.consecutiveIdleScans = 0;
|
|
449
|
+
return DaemonDesirePathService.BASE_INTERVAL_MS;
|
|
450
|
+
}
|
|
451
|
+
const backoff = DaemonDesirePathService.BASE_INTERVAL_MS * Math.pow(2, this.consecutiveIdleScans);
|
|
452
|
+
return Math.min(backoff, DaemonDesirePathService.MAX_INTERVAL_MS);
|
|
453
|
+
}
|
|
454
|
+
start() {
|
|
455
|
+
if (this.isRunning || this.isOptedOut()) {
|
|
456
|
+
if (this.isOptedOut()) {
|
|
457
|
+
this.onLog("INFO", "Desire-path detection disabled");
|
|
458
|
+
}
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
this.isRunning = true;
|
|
462
|
+
mkdirSync(DP_DIR, { recursive: true });
|
|
463
|
+
this.onLog(
|
|
464
|
+
"INFO",
|
|
465
|
+
"Desire-path service started (adaptive backoff: 1h active, up to 12h idle)"
|
|
466
|
+
);
|
|
467
|
+
this.scanTimeout = setTimeout(() => {
|
|
468
|
+
if (!this.isRunning) return;
|
|
469
|
+
this.runScanAndScheduleNext();
|
|
470
|
+
}, 12e4);
|
|
471
|
+
if (this.scanTimeout.unref) this.scanTimeout.unref();
|
|
472
|
+
}
|
|
473
|
+
stop() {
|
|
474
|
+
if (this.scanTimeout) {
|
|
475
|
+
clearTimeout(this.scanTimeout);
|
|
476
|
+
this.scanTimeout = void 0;
|
|
477
|
+
}
|
|
478
|
+
this.isRunning = false;
|
|
479
|
+
}
|
|
480
|
+
runScanAndScheduleNext() {
|
|
481
|
+
this.runScan();
|
|
482
|
+
if (!this.isRunning) return;
|
|
483
|
+
const nextMs = this.getNextInterval();
|
|
484
|
+
this.onLog("DEBUG", "Next scan scheduled", {
|
|
485
|
+
next_min: Math.round(nextMs / 6e4),
|
|
486
|
+
idle_scans: this.consecutiveIdleScans
|
|
487
|
+
});
|
|
488
|
+
this.scanTimeout = setTimeout(() => {
|
|
489
|
+
if (!this.isRunning) return;
|
|
490
|
+
this.runScanAndScheduleNext();
|
|
491
|
+
}, nextMs);
|
|
492
|
+
if (this.scanTimeout.unref) this.scanTimeout.unref();
|
|
493
|
+
}
|
|
494
|
+
runScan() {
|
|
495
|
+
const prevActionsLogged = this.state.actionsLogged;
|
|
496
|
+
try {
|
|
497
|
+
const patterns = this.detectPatterns();
|
|
498
|
+
if (patterns.length > 0) {
|
|
499
|
+
const suggestions = this.generateSuggestions(patterns);
|
|
500
|
+
this.onLog("INFO", "Desire-path scan complete", {
|
|
501
|
+
patterns: patterns.length,
|
|
502
|
+
suggestions: suggestions.length,
|
|
503
|
+
topPattern: patterns[0] ? sequenceHash(patterns[0].sequence) : "none",
|
|
504
|
+
interval_min: Math.round(this.getNextInterval() / 6e4)
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
this.state.lastScanTime = Date.now();
|
|
508
|
+
if (this.state.actionsLogged === prevActionsLogged) {
|
|
509
|
+
this.consecutiveIdleScans++;
|
|
510
|
+
} else {
|
|
511
|
+
this.consecutiveIdleScans = 0;
|
|
512
|
+
}
|
|
513
|
+
} catch (err) {
|
|
514
|
+
this.addError(String(err));
|
|
515
|
+
this.onLog("ERROR", "Desire-path scan failed", { error: String(err) });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
addError(err) {
|
|
519
|
+
this.state.errors.push(err);
|
|
520
|
+
if (this.state.errors.length > 10) {
|
|
521
|
+
this.state.errors = this.state.errors.slice(-10);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
getState() {
|
|
525
|
+
return { ...this.state };
|
|
526
|
+
}
|
|
527
|
+
/** Get current suggestions for CLI/MCP consumption. */
|
|
528
|
+
getSuggestions() {
|
|
529
|
+
try {
|
|
530
|
+
if (!existsSync(SUGGESTIONS_DIR)) return [];
|
|
531
|
+
const files = readdirSync(SUGGESTIONS_DIR).filter(
|
|
532
|
+
(f) => f.endsWith(".skill.md")
|
|
533
|
+
);
|
|
534
|
+
return files.map((f) => {
|
|
535
|
+
const content = readFileSync(join(SUGGESTIONS_DIR, f), "utf-8");
|
|
536
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
537
|
+
if (!match) return null;
|
|
538
|
+
try {
|
|
539
|
+
const lines = match[1].split("\n");
|
|
540
|
+
const meta = {};
|
|
541
|
+
for (const line of lines) {
|
|
542
|
+
const kv = line.match(/^(\w[\w_-]*):\s*(.*)/);
|
|
543
|
+
if (kv) meta[kv[1]] = kv[2].trim().replace(/^["']|["']$/g, "");
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
name: meta.name || basename(f, ".skill.md"),
|
|
547
|
+
description: meta.description || "",
|
|
548
|
+
pattern_id: meta.pattern_id || "",
|
|
549
|
+
confidence: parseFloat(meta.confidence || "0"),
|
|
550
|
+
generated_at: meta.generated_at || "",
|
|
551
|
+
inputs: [],
|
|
552
|
+
outputs: [],
|
|
553
|
+
steps: []
|
|
554
|
+
};
|
|
555
|
+
} catch {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
}).filter(Boolean);
|
|
559
|
+
} catch {
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
export {
|
|
565
|
+
DaemonDesirePathService
|
|
566
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
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 { existsSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { refreshCurrentRepoPullRequestState } from "../../integrations/github/pr-state.js";
|
|
9
|
+
import { canonicalStateStore } from "../../core/shared-state/canonical-store.js";
|
|
10
|
+
class DaemonGitHubService {
|
|
11
|
+
config;
|
|
12
|
+
state;
|
|
13
|
+
intervalId;
|
|
14
|
+
isRunning = false;
|
|
15
|
+
onLog;
|
|
16
|
+
constructor(config, onLog) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.onLog = onLog;
|
|
19
|
+
this.state = {
|
|
20
|
+
lastSyncTime: 0,
|
|
21
|
+
syncCount: 0,
|
|
22
|
+
errors: []
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async start() {
|
|
26
|
+
if (this.isRunning || !this.config.enabled) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!this.isGitHubConfigured()) {
|
|
30
|
+
this.onLog("WARN", "GitHub CLI not configured, skipping github service");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.isRunning = true;
|
|
34
|
+
const intervalMs = this.config.interval * 60 * 1e3;
|
|
35
|
+
this.onLog("INFO", "GitHub service started", {
|
|
36
|
+
interval: this.config.interval
|
|
37
|
+
});
|
|
38
|
+
await this.performSync();
|
|
39
|
+
this.intervalId = setInterval(async () => {
|
|
40
|
+
await this.performSync();
|
|
41
|
+
}, intervalMs);
|
|
42
|
+
}
|
|
43
|
+
stop() {
|
|
44
|
+
if (this.intervalId) {
|
|
45
|
+
clearInterval(this.intervalId);
|
|
46
|
+
this.intervalId = void 0;
|
|
47
|
+
}
|
|
48
|
+
this.isRunning = false;
|
|
49
|
+
this.onLog("INFO", "GitHub service stopped");
|
|
50
|
+
}
|
|
51
|
+
getState() {
|
|
52
|
+
return {
|
|
53
|
+
...this.state,
|
|
54
|
+
nextSyncTime: this.isRunning ? this.state.lastSyncTime + this.config.interval * 60 * 1e3 : void 0
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async forceSync() {
|
|
58
|
+
await this.performSync();
|
|
59
|
+
}
|
|
60
|
+
async performSync() {
|
|
61
|
+
if (!this.isRunning) return;
|
|
62
|
+
try {
|
|
63
|
+
const projectRoots = await this.getProjectRoots();
|
|
64
|
+
this.state.lastProjectsScanned = projectRoots.length;
|
|
65
|
+
if (projectRoots.length === 0) {
|
|
66
|
+
this.onLog("DEBUG", "No active project roots found for GitHub sync");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let synced = false;
|
|
70
|
+
for (const projectRoot of projectRoots) {
|
|
71
|
+
const projection = await refreshCurrentRepoPullRequestState(projectRoot);
|
|
72
|
+
if (!projection) {
|
|
73
|
+
this.onLog("DEBUG", "No GitHub PR projection available", {
|
|
74
|
+
projectRoot
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
synced = true;
|
|
79
|
+
this.state.syncCount++;
|
|
80
|
+
this.state.lastSyncTime = Date.now();
|
|
81
|
+
this.state.lastProjectionState = projection.state;
|
|
82
|
+
this.onLog("INFO", "GitHub PR projection refreshed", {
|
|
83
|
+
projectRoot,
|
|
84
|
+
repo: projection.repo,
|
|
85
|
+
branch: projection.branch,
|
|
86
|
+
prNumber: projection.prNumber,
|
|
87
|
+
state: projection.state
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (!synced) {
|
|
91
|
+
this.state.lastSyncTime = Date.now();
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
95
|
+
this.state.errors.push(errorMsg);
|
|
96
|
+
this.onLog("ERROR", "GitHub sync failed", { error: errorMsg });
|
|
97
|
+
if (this.state.errors.length > 10) {
|
|
98
|
+
this.state.errors = this.state.errors.slice(-10);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
isGitHubConfigured() {
|
|
103
|
+
try {
|
|
104
|
+
return existsSync(join(homedir(), ".config", "gh", "hosts.yml"));
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async getProjectRoots() {
|
|
110
|
+
const roots = /* @__PURE__ */ new Set();
|
|
111
|
+
const activeProjectPaths = await canonicalStateStore.listActiveProjectPaths();
|
|
112
|
+
for (const projectPath of activeProjectPaths) {
|
|
113
|
+
if (existsSync(join(projectPath, ".git"))) {
|
|
114
|
+
roots.add(projectPath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const cwd = process.cwd();
|
|
118
|
+
if (existsSync(join(cwd, ".git"))) {
|
|
119
|
+
roots.add(cwd);
|
|
120
|
+
}
|
|
121
|
+
return Array.from(roots).sort();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
DaemonGitHubService
|
|
126
|
+
};
|