@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,578 @@
|
|
|
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
|
+
mkdirSync,
|
|
8
|
+
appendFileSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
writeFileSync
|
|
11
|
+
} from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { randomUUID, createHash } from "crypto";
|
|
15
|
+
import { logger } from "../../core/monitoring/logger.js";
|
|
16
|
+
const SM_DIR = join(homedir(), ".stackmemory");
|
|
17
|
+
const DP_DIR = join(SM_DIR, "desire-paths");
|
|
18
|
+
const WORKFLOW_DIR = join(SM_DIR, "workflows");
|
|
19
|
+
const STREAM_FILE = join(DP_DIR, "action-stream.jsonl");
|
|
20
|
+
const WORKFLOW_CACHE_FILE = join(WORKFLOW_DIR, "cached-workflows.json");
|
|
21
|
+
const BENCHMARK_FILE = join(WORKFLOW_DIR, "benchmarks.jsonl");
|
|
22
|
+
class StagehandWorkflowCapture {
|
|
23
|
+
stagehand;
|
|
24
|
+
// Stagehand instance (peer dep, not typed)
|
|
25
|
+
sessionId;
|
|
26
|
+
recording = false;
|
|
27
|
+
currentWorkflow = null;
|
|
28
|
+
steps = [];
|
|
29
|
+
constructor(stagehandInstance, sessionId) {
|
|
30
|
+
this.stagehand = stagehandInstance;
|
|
31
|
+
this.sessionId = sessionId || `wf-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
32
|
+
ensureDir(DP_DIR);
|
|
33
|
+
ensureDir(WORKFLOW_DIR);
|
|
34
|
+
}
|
|
35
|
+
/** Start recording a workflow */
|
|
36
|
+
startCapture(name) {
|
|
37
|
+
this.recording = true;
|
|
38
|
+
this.steps = [];
|
|
39
|
+
const page = this.stagehand.context?.pages?.()?.[0];
|
|
40
|
+
const startUrl = page?.url?.() || "about:blank";
|
|
41
|
+
this.currentWorkflow = {
|
|
42
|
+
id: randomUUID(),
|
|
43
|
+
name,
|
|
44
|
+
startUrl,
|
|
45
|
+
steps: [],
|
|
46
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
47
|
+
totalDuration: 0,
|
|
48
|
+
totalTokens: 0,
|
|
49
|
+
sessionId: this.sessionId
|
|
50
|
+
};
|
|
51
|
+
logger.info("Workflow capture started", { name, startUrl });
|
|
52
|
+
}
|
|
53
|
+
/** Navigate to a URL */
|
|
54
|
+
async navigate(url) {
|
|
55
|
+
const start = Date.now();
|
|
56
|
+
const page = this.stagehand.context.pages()[0];
|
|
57
|
+
await page.goto(url);
|
|
58
|
+
const dur = Date.now() - start;
|
|
59
|
+
this.recordStep({
|
|
60
|
+
type: "navigate",
|
|
61
|
+
instruction: url,
|
|
62
|
+
url,
|
|
63
|
+
duration: dur,
|
|
64
|
+
tokens: 0,
|
|
65
|
+
cached: false
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Execute a Stagehand act() — natural language browser action */
|
|
69
|
+
async act(instruction, options) {
|
|
70
|
+
const start = Date.now();
|
|
71
|
+
const result = await this.stagehand.act(instruction, options);
|
|
72
|
+
const dur = Date.now() - start;
|
|
73
|
+
const page = this.stagehand.context.pages()[0];
|
|
74
|
+
const url = page?.url?.() || "";
|
|
75
|
+
const tokens = estimateActTokens(instruction);
|
|
76
|
+
this.recordStep({
|
|
77
|
+
type: "act",
|
|
78
|
+
instruction,
|
|
79
|
+
url,
|
|
80
|
+
duration: dur,
|
|
81
|
+
tokens,
|
|
82
|
+
cached: false,
|
|
83
|
+
// TODO: detect from Stagehand cache hits
|
|
84
|
+
result
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
/** Execute a Stagehand extract() — structured data extraction */
|
|
89
|
+
async extract(instruction, schema, options) {
|
|
90
|
+
const start = Date.now();
|
|
91
|
+
const result = await this.stagehand.extract(instruction, schema, options);
|
|
92
|
+
const dur = Date.now() - start;
|
|
93
|
+
const page = this.stagehand.context.pages()[0];
|
|
94
|
+
const url = page?.url?.() || "";
|
|
95
|
+
const tokens = estimateActTokens(instruction);
|
|
96
|
+
this.recordStep({
|
|
97
|
+
type: "extract",
|
|
98
|
+
instruction,
|
|
99
|
+
url,
|
|
100
|
+
duration: dur,
|
|
101
|
+
tokens,
|
|
102
|
+
cached: false,
|
|
103
|
+
result
|
|
104
|
+
});
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
/** Execute a Stagehand observe() — discover available actions */
|
|
108
|
+
async observe(instruction) {
|
|
109
|
+
const start = Date.now();
|
|
110
|
+
const result = await this.stagehand.observe(instruction);
|
|
111
|
+
const dur = Date.now() - start;
|
|
112
|
+
const page = this.stagehand.context.pages()[0];
|
|
113
|
+
const url = page?.url?.() || "";
|
|
114
|
+
this.recordStep({
|
|
115
|
+
type: "observe",
|
|
116
|
+
instruction,
|
|
117
|
+
url,
|
|
118
|
+
duration: dur,
|
|
119
|
+
tokens: estimateActTokens(instruction),
|
|
120
|
+
cached: false,
|
|
121
|
+
result
|
|
122
|
+
});
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
/** Stop recording and return the captured workflow */
|
|
126
|
+
stopCapture() {
|
|
127
|
+
if (!this.currentWorkflow) return null;
|
|
128
|
+
this.recording = false;
|
|
129
|
+
this.currentWorkflow.steps = [...this.steps];
|
|
130
|
+
this.currentWorkflow.totalDuration = this.steps.reduce(
|
|
131
|
+
(sum, s) => sum + s.duration,
|
|
132
|
+
0
|
|
133
|
+
);
|
|
134
|
+
this.currentWorkflow.totalTokens = this.steps.reduce(
|
|
135
|
+
(sum, s) => sum + s.tokens,
|
|
136
|
+
0
|
|
137
|
+
);
|
|
138
|
+
saveWorkflow(this.currentWorkflow);
|
|
139
|
+
const desirePathPattern = workflowToDesirePathPattern(this.currentWorkflow);
|
|
140
|
+
appendDesirePathPattern(desirePathPattern);
|
|
141
|
+
logger.info("Workflow capture stopped", {
|
|
142
|
+
id: this.currentWorkflow.id,
|
|
143
|
+
steps: this.steps.length,
|
|
144
|
+
duration: this.currentWorkflow.totalDuration
|
|
145
|
+
});
|
|
146
|
+
const wf = this.currentWorkflow;
|
|
147
|
+
this.currentWorkflow = null;
|
|
148
|
+
this.steps = [];
|
|
149
|
+
return wf;
|
|
150
|
+
}
|
|
151
|
+
/** Record a step + emit to action-stream */
|
|
152
|
+
recordStep(step) {
|
|
153
|
+
if (this.recording) {
|
|
154
|
+
this.steps.push(step);
|
|
155
|
+
}
|
|
156
|
+
const entry = {
|
|
157
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
158
|
+
sid: this.sessionId,
|
|
159
|
+
tool: `stagehand:${step.type}`,
|
|
160
|
+
target: sanitizeInstruction(step.instruction),
|
|
161
|
+
dur: step.duration,
|
|
162
|
+
meta: {
|
|
163
|
+
url: step.url,
|
|
164
|
+
cached: step.cached,
|
|
165
|
+
tokens: step.tokens,
|
|
166
|
+
success: !step.result?.error
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
emitToActionStream(entry);
|
|
170
|
+
}
|
|
171
|
+
/** Get the underlying Stagehand instance for direct access */
|
|
172
|
+
get instance() {
|
|
173
|
+
return this.stagehand;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
class WorkflowCache {
|
|
177
|
+
workflows = /* @__PURE__ */ new Map();
|
|
178
|
+
constructor() {
|
|
179
|
+
this.load();
|
|
180
|
+
}
|
|
181
|
+
/** Load cached workflows from disk */
|
|
182
|
+
load() {
|
|
183
|
+
if (!existsSync(WORKFLOW_CACHE_FILE)) return;
|
|
184
|
+
try {
|
|
185
|
+
const data = JSON.parse(readFileSync(WORKFLOW_CACHE_FILE, "utf-8"));
|
|
186
|
+
if (Array.isArray(data)) {
|
|
187
|
+
for (const entry of data) {
|
|
188
|
+
this.workflows.set(entry.id, entry);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Save to disk */
|
|
195
|
+
save() {
|
|
196
|
+
ensureDir(WORKFLOW_DIR);
|
|
197
|
+
writeFileSync(
|
|
198
|
+
WORKFLOW_CACHE_FILE,
|
|
199
|
+
JSON.stringify([...this.workflows.values()], null, 2)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
/** Store a captured workflow as a cached entry */
|
|
203
|
+
cacheWorkflow(workflow) {
|
|
204
|
+
const entry = {
|
|
205
|
+
version: 1,
|
|
206
|
+
id: workflow.id,
|
|
207
|
+
name: workflow.name,
|
|
208
|
+
startUrl: workflow.startUrl,
|
|
209
|
+
steps: workflow.steps.map((s) => ({
|
|
210
|
+
type: s.type,
|
|
211
|
+
instruction: s.instruction,
|
|
212
|
+
url: s.url
|
|
213
|
+
})),
|
|
214
|
+
capturedAt: workflow.capturedAt,
|
|
215
|
+
replayCount: 0,
|
|
216
|
+
avgDuration: workflow.totalDuration,
|
|
217
|
+
successRate: 1
|
|
218
|
+
};
|
|
219
|
+
this.workflows.set(entry.id, entry);
|
|
220
|
+
this.save();
|
|
221
|
+
return entry;
|
|
222
|
+
}
|
|
223
|
+
/** Find a cached workflow by name (fuzzy match) */
|
|
224
|
+
findByName(query) {
|
|
225
|
+
const lower = query.toLowerCase();
|
|
226
|
+
for (const entry of this.workflows.values()) {
|
|
227
|
+
if (entry.name.toLowerCase().includes(lower)) return entry;
|
|
228
|
+
}
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
/** Find workflows matching a URL pattern */
|
|
232
|
+
findByUrl(url) {
|
|
233
|
+
const host = extractHost(url);
|
|
234
|
+
return [...this.workflows.values()].filter(
|
|
235
|
+
(w) => extractHost(w.startUrl) === host
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
/** List all cached workflows */
|
|
239
|
+
list() {
|
|
240
|
+
return [...this.workflows.values()].sort(
|
|
241
|
+
(a, b) => b.replayCount - a.replayCount
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
/** Update replay stats after a replay attempt */
|
|
245
|
+
recordReplay(id, success, duration) {
|
|
246
|
+
const entry = this.workflows.get(id);
|
|
247
|
+
if (!entry) return;
|
|
248
|
+
entry.replayCount++;
|
|
249
|
+
entry.lastReplayAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
250
|
+
entry.avgDuration = (entry.avgDuration * (entry.replayCount - 1) + duration) / entry.replayCount;
|
|
251
|
+
entry.successRate = (entry.successRate * (entry.replayCount - 1) + (success ? 1 : 0)) / entry.replayCount;
|
|
252
|
+
this.save();
|
|
253
|
+
}
|
|
254
|
+
get(id) {
|
|
255
|
+
return this.workflows.get(id);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
class WorkflowReplayer {
|
|
259
|
+
stagehand;
|
|
260
|
+
cache;
|
|
261
|
+
constructor(stagehandInstance, cache) {
|
|
262
|
+
this.stagehand = stagehandInstance;
|
|
263
|
+
this.cache = cache || new WorkflowCache();
|
|
264
|
+
}
|
|
265
|
+
/** Replay a workflow by ID */
|
|
266
|
+
async replay(workflowId, mode = "hybrid", variables) {
|
|
267
|
+
const entry = this.cache.get(workflowId);
|
|
268
|
+
if (!entry) {
|
|
269
|
+
return {
|
|
270
|
+
success: false,
|
|
271
|
+
error: "Workflow not found",
|
|
272
|
+
duration: 0,
|
|
273
|
+
steps: 0,
|
|
274
|
+
selfHealed: false
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return this.replayEntry(entry, mode, variables);
|
|
278
|
+
}
|
|
279
|
+
/** Replay by name (fuzzy match) */
|
|
280
|
+
async replayByName(name, mode = "hybrid", variables) {
|
|
281
|
+
const entry = this.cache.findByName(name);
|
|
282
|
+
if (!entry) {
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
error: `No workflow matching "${name}"`,
|
|
286
|
+
duration: 0,
|
|
287
|
+
steps: 0,
|
|
288
|
+
selfHealed: false
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return this.replayEntry(entry, mode, variables);
|
|
292
|
+
}
|
|
293
|
+
async replayEntry(entry, mode, variables) {
|
|
294
|
+
const start = Date.now();
|
|
295
|
+
let selfHealed = false;
|
|
296
|
+
let stepsCompleted = 0;
|
|
297
|
+
try {
|
|
298
|
+
const page = this.stagehand.context.pages()[0];
|
|
299
|
+
let startUrl = entry.startUrl;
|
|
300
|
+
if (variables) {
|
|
301
|
+
startUrl = substituteVars(startUrl, variables);
|
|
302
|
+
}
|
|
303
|
+
await page.goto(startUrl);
|
|
304
|
+
for (const step of entry.steps) {
|
|
305
|
+
let instruction = step.instruction;
|
|
306
|
+
if (variables) {
|
|
307
|
+
instruction = substituteVars(instruction, variables);
|
|
308
|
+
}
|
|
309
|
+
if (step.type === "navigate" && step.url) {
|
|
310
|
+
const url = variables ? substituteVars(step.url, variables) : step.url;
|
|
311
|
+
await page.goto(url);
|
|
312
|
+
} else if (step.type === "act") {
|
|
313
|
+
if (mode === "cached" && step.actions?.length) {
|
|
314
|
+
try {
|
|
315
|
+
await replayActions(page, step.actions);
|
|
316
|
+
} catch {
|
|
317
|
+
if (mode === "cached")
|
|
318
|
+
throw new Error("Cache miss \u2014 selector changed");
|
|
319
|
+
selfHealed = true;
|
|
320
|
+
await this.stagehand.act(instruction);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
await this.stagehand.act(instruction);
|
|
324
|
+
}
|
|
325
|
+
} else if (step.type === "extract") {
|
|
326
|
+
await this.stagehand.extract(
|
|
327
|
+
instruction,
|
|
328
|
+
step.schema ? JSON.parse(step.schema) : void 0
|
|
329
|
+
);
|
|
330
|
+
} else if (step.type === "observe") {
|
|
331
|
+
await this.stagehand.observe(instruction);
|
|
332
|
+
}
|
|
333
|
+
stepsCompleted++;
|
|
334
|
+
}
|
|
335
|
+
const duration = Date.now() - start;
|
|
336
|
+
this.cache.recordReplay(entry.id, true, duration);
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
duration,
|
|
340
|
+
steps: stepsCompleted,
|
|
341
|
+
selfHealed
|
|
342
|
+
};
|
|
343
|
+
} catch (error) {
|
|
344
|
+
const duration = Date.now() - start;
|
|
345
|
+
this.cache.recordReplay(entry.id, false, duration);
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
error: error.message,
|
|
349
|
+
duration,
|
|
350
|
+
steps: stepsCompleted,
|
|
351
|
+
selfHealed
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
class WorkflowBenchmark {
|
|
357
|
+
results = [];
|
|
358
|
+
/** Run a benchmark with Stagehand AI mode */
|
|
359
|
+
async benchmarkStagehandAI(name, stagehand, steps) {
|
|
360
|
+
const start = Date.now();
|
|
361
|
+
let success = true;
|
|
362
|
+
let error;
|
|
363
|
+
let totalTokens = 0;
|
|
364
|
+
try {
|
|
365
|
+
const page = stagehand.context.pages()[0];
|
|
366
|
+
for (const step of steps) {
|
|
367
|
+
if (step.type === "navigate") {
|
|
368
|
+
await page.goto(step.instruction);
|
|
369
|
+
} else if (step.type === "act") {
|
|
370
|
+
await stagehand.act(step.instruction);
|
|
371
|
+
totalTokens += estimateActTokens(step.instruction);
|
|
372
|
+
} else if (step.type === "extract") {
|
|
373
|
+
await stagehand.extract(step.instruction, step.schema);
|
|
374
|
+
totalTokens += estimateActTokens(step.instruction);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch (e) {
|
|
378
|
+
success = false;
|
|
379
|
+
error = e.message;
|
|
380
|
+
}
|
|
381
|
+
const result = {
|
|
382
|
+
workflow: name,
|
|
383
|
+
approach: "stagehand-ai",
|
|
384
|
+
duration: Date.now() - start,
|
|
385
|
+
tokens: totalTokens,
|
|
386
|
+
success,
|
|
387
|
+
selfHealed: false,
|
|
388
|
+
steps: steps.length,
|
|
389
|
+
error
|
|
390
|
+
};
|
|
391
|
+
this.results.push(result);
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
/** Run a benchmark with cached replay */
|
|
395
|
+
async benchmarkStagehandCached(name, replayer, workflowId, variables) {
|
|
396
|
+
const replayResult = await replayer.replay(workflowId, "cached", variables);
|
|
397
|
+
const result = {
|
|
398
|
+
workflow: name,
|
|
399
|
+
approach: "stagehand-cached",
|
|
400
|
+
duration: replayResult.duration,
|
|
401
|
+
tokens: 0,
|
|
402
|
+
// Cached = no AI tokens
|
|
403
|
+
success: replayResult.success,
|
|
404
|
+
selfHealed: replayResult.selfHealed,
|
|
405
|
+
steps: replayResult.steps,
|
|
406
|
+
error: replayResult.error
|
|
407
|
+
};
|
|
408
|
+
this.results.push(result);
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
/** Run a benchmark with a Playwright code function */
|
|
412
|
+
async benchmarkPlaywright(name, fn, stepCount) {
|
|
413
|
+
const start = Date.now();
|
|
414
|
+
let success = true;
|
|
415
|
+
let error;
|
|
416
|
+
try {
|
|
417
|
+
await fn();
|
|
418
|
+
} catch (e) {
|
|
419
|
+
success = false;
|
|
420
|
+
error = e.message;
|
|
421
|
+
}
|
|
422
|
+
const result = {
|
|
423
|
+
workflow: name,
|
|
424
|
+
approach: "playwright-code",
|
|
425
|
+
duration: Date.now() - start,
|
|
426
|
+
tokens: 0,
|
|
427
|
+
success,
|
|
428
|
+
selfHealed: false,
|
|
429
|
+
steps: stepCount,
|
|
430
|
+
error
|
|
431
|
+
};
|
|
432
|
+
this.results.push(result);
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
/** Get all results */
|
|
436
|
+
getResults() {
|
|
437
|
+
return [...this.results];
|
|
438
|
+
}
|
|
439
|
+
/** Print a comparison table */
|
|
440
|
+
formatTable() {
|
|
441
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
442
|
+
for (const r of this.results) {
|
|
443
|
+
const list = grouped.get(r.workflow) || [];
|
|
444
|
+
list.push(r);
|
|
445
|
+
grouped.set(r.workflow, list);
|
|
446
|
+
}
|
|
447
|
+
const lines = [];
|
|
448
|
+
lines.push(
|
|
449
|
+
"| Workflow | Approach | Duration | Tokens | Success | Self-Healed |"
|
|
450
|
+
);
|
|
451
|
+
lines.push(
|
|
452
|
+
"|----------|----------|----------|--------|---------|-------------|"
|
|
453
|
+
);
|
|
454
|
+
for (const [name, results] of grouped) {
|
|
455
|
+
for (const r of results) {
|
|
456
|
+
lines.push(
|
|
457
|
+
`| ${name} | ${r.approach} | ${r.duration}ms | ${r.tokens} | ${r.success ? "Y" : "N"} | ${r.selfHealed ? "Y" : "N"} |`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return lines.join("\n");
|
|
462
|
+
}
|
|
463
|
+
/** Save results to JSONL */
|
|
464
|
+
save() {
|
|
465
|
+
ensureDir(WORKFLOW_DIR);
|
|
466
|
+
for (const result of this.results) {
|
|
467
|
+
appendFileSync(BENCHMARK_FILE, JSON.stringify(result) + "\n");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function ensureDir(dir) {
|
|
472
|
+
if (!existsSync(dir)) {
|
|
473
|
+
mkdirSync(dir, { recursive: true });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function sanitizeInstruction(instruction) {
|
|
477
|
+
return instruction.slice(0, 100).replace(/[\n\r]/g, " ");
|
|
478
|
+
}
|
|
479
|
+
function estimateActTokens(instruction) {
|
|
480
|
+
return Math.ceil(instruction.length / 4) + 2e3;
|
|
481
|
+
}
|
|
482
|
+
function extractHost(url) {
|
|
483
|
+
try {
|
|
484
|
+
return new URL(url).host;
|
|
485
|
+
} catch {
|
|
486
|
+
return url;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function substituteVars(template, vars) {
|
|
490
|
+
let result = template;
|
|
491
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
492
|
+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
|
493
|
+
}
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
function emitToActionStream(entry) {
|
|
497
|
+
try {
|
|
498
|
+
ensureDir(DP_DIR);
|
|
499
|
+
appendFileSync(STREAM_FILE, JSON.stringify(entry) + "\n");
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function workflowToDesirePathPattern(workflow) {
|
|
504
|
+
return {
|
|
505
|
+
id: workflow.id,
|
|
506
|
+
sequence: workflow.steps.map(
|
|
507
|
+
(s) => `stagehand:${s.type}:${sanitizeInstruction(s.instruction)}`
|
|
508
|
+
),
|
|
509
|
+
frequency: 1,
|
|
510
|
+
sessions: 1,
|
|
511
|
+
score: workflow.steps.length,
|
|
512
|
+
// longer = more valuable
|
|
513
|
+
first_seen: workflow.capturedAt,
|
|
514
|
+
last_seen: workflow.capturedAt,
|
|
515
|
+
source: "stagehand"
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function appendDesirePathPattern(pattern) {
|
|
519
|
+
const patternsFile = join(DP_DIR, "patterns.json");
|
|
520
|
+
let patterns = [];
|
|
521
|
+
if (existsSync(patternsFile)) {
|
|
522
|
+
try {
|
|
523
|
+
patterns = JSON.parse(readFileSync(patternsFile, "utf-8"));
|
|
524
|
+
} catch {
|
|
525
|
+
patterns = [];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const seqHash = createHash("sha256").update(pattern.sequence.join("|")).digest("hex").slice(0, 16);
|
|
529
|
+
const existing = patterns.find((p) => {
|
|
530
|
+
const h = createHash("sha256").update((p.sequence || []).join("|")).digest("hex").slice(0, 16);
|
|
531
|
+
return h === seqHash;
|
|
532
|
+
});
|
|
533
|
+
if (existing) {
|
|
534
|
+
existing.frequency = (existing.frequency || 0) + 1;
|
|
535
|
+
existing.sessions = (existing.sessions || 0) + 1;
|
|
536
|
+
existing.score = existing.frequency * existing.sessions;
|
|
537
|
+
existing.last_seen = pattern.last_seen;
|
|
538
|
+
} else {
|
|
539
|
+
patterns.push(pattern);
|
|
540
|
+
}
|
|
541
|
+
ensureDir(DP_DIR);
|
|
542
|
+
writeFileSync(patternsFile, JSON.stringify(patterns, null, 2));
|
|
543
|
+
}
|
|
544
|
+
async function replayActions(page, actions) {
|
|
545
|
+
for (const action of actions) {
|
|
546
|
+
if (!action.selector) continue;
|
|
547
|
+
const element = await page.locator(action.selector);
|
|
548
|
+
switch (action.action) {
|
|
549
|
+
case "click":
|
|
550
|
+
await element.click();
|
|
551
|
+
break;
|
|
552
|
+
case "fill":
|
|
553
|
+
case "type":
|
|
554
|
+
if (action.text) await element.fill(action.text);
|
|
555
|
+
break;
|
|
556
|
+
case "selectOption":
|
|
557
|
+
if (action.args?.[0]) await element.selectOption(action.args[0]);
|
|
558
|
+
break;
|
|
559
|
+
default:
|
|
560
|
+
if (action.action && typeof element[action.action] === "function") {
|
|
561
|
+
await element[action.action](...action.args || []);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function saveWorkflow(workflow) {
|
|
567
|
+
const file = join(WORKFLOW_DIR, `${workflow.id}.json`);
|
|
568
|
+
ensureDir(WORKFLOW_DIR);
|
|
569
|
+
writeFileSync(file, JSON.stringify(workflow, null, 2));
|
|
570
|
+
const cache = new WorkflowCache();
|
|
571
|
+
cache.cacheWorkflow(workflow);
|
|
572
|
+
}
|
|
573
|
+
export {
|
|
574
|
+
StagehandWorkflowCapture,
|
|
575
|
+
WorkflowBenchmark,
|
|
576
|
+
WorkflowCache,
|
|
577
|
+
WorkflowReplayer
|
|
578
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { execSync } from "child_process";
|
|
6
|
+
import { TmuxAdapter } from "./screen-adapter.js";
|
|
7
|
+
function createAdapter(config) {
|
|
8
|
+
const mode = config.mode === "auto" ? detectMode(config) : config.mode;
|
|
9
|
+
switch (mode) {
|
|
10
|
+
case "tmux":
|
|
11
|
+
return new TmuxAdapter(config.sessionName, "0");
|
|
12
|
+
case "desktop": {
|
|
13
|
+
if (!config.llmConfig?.apiKey) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Desktop mode requires ANTHROPIC_API_KEY for screenshot interpretation.\nSet it via: export ANTHROPIC_API_KEY=sk-ant-..."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const { DesktopAdapter } = require("./desktop-adapter.js");
|
|
19
|
+
return new DesktopAdapter(config.appName ?? "Claude", config.llmConfig);
|
|
20
|
+
}
|
|
21
|
+
case "browser":
|
|
22
|
+
throw new Error(
|
|
23
|
+
"Browser mode requires a Playwright page instance.\nUse BrowserAdapter directly with an initialized page,\nor use: stackmemory operator start --mode browser --url https://claude.ai/code"
|
|
24
|
+
);
|
|
25
|
+
default:
|
|
26
|
+
return new TmuxAdapter(config.sessionName, "0");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function detectMode(config) {
|
|
30
|
+
if (isTmuxAvailable()) {
|
|
31
|
+
return "tmux";
|
|
32
|
+
}
|
|
33
|
+
if (process.platform === "darwin" && isClaudeDesktopRunning(config.appName)) {
|
|
34
|
+
return "desktop";
|
|
35
|
+
}
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
"[operator] Warning: tmux not found and no desktop app detected.\n Install tmux: brew install tmux\n Or use --mode desktop/browser\n"
|
|
38
|
+
);
|
|
39
|
+
return "tmux";
|
|
40
|
+
}
|
|
41
|
+
function isTmuxAvailable() {
|
|
42
|
+
try {
|
|
43
|
+
execSync("which tmux", { stdio: "ignore" });
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function isClaudeDesktopRunning(appName = "Claude") {
|
|
50
|
+
try {
|
|
51
|
+
const result = execSync(
|
|
52
|
+
`osascript -e 'tell application "System Events" to (name of processes) contains "${appName}"'`,
|
|
53
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
54
|
+
).trim();
|
|
55
|
+
return result === "true";
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
createAdapter
|
|
62
|
+
};
|