create-walle 0.9.13 → 0.9.15
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 +8 -3
- package/bin/create-walle.js +232 -32
- package/bin/mcp-inject.js +18 -53
- package/package.json +3 -1
- package/template/claude-task-manager/api-prompts.js +11 -2
- package/template/claude-task-manager/approval-agent.js +7 -0
- package/template/claude-task-manager/db.js +94 -75
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
- package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
- package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
- package/template/claude-task-manager/fuzzy-utils.js +10 -2
- package/template/claude-task-manager/git-utils.js +140 -10
- package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
- package/template/claude-task-manager/lib/agent-presets.js +38 -5
- package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
- package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
- package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
- package/template/claude-task-manager/lib/session-history.js +309 -16
- package/template/claude-task-manager/lib/session-standup.js +409 -0
- package/template/claude-task-manager/lib/session-stream.js +253 -20
- package/template/claude-task-manager/lib/standup-attention.js +200 -0
- package/template/claude-task-manager/lib/status-hooks.js +8 -2
- package/template/claude-task-manager/lib/update-telemetry.js +114 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
- package/template/claude-task-manager/lib/walle-default-model.js +55 -0
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
- package/template/claude-task-manager/lib/walle-transcript.js +1 -3
- package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
- package/template/claude-task-manager/package.json +1 -0
- package/template/claude-task-manager/providers/codex-mcp.js +104 -0
- package/template/claude-task-manager/providers/index.js +2 -0
- package/template/claude-task-manager/public/css/setup.css +2 -1
- package/template/claude-task-manager/public/css/walle.css +71 -0
- package/template/claude-task-manager/public/index.html +2388 -429
- package/template/claude-task-manager/public/js/message-renderer.js +314 -35
- package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
- package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
- package/template/claude-task-manager/public/js/setup.js +62 -19
- package/template/claude-task-manager/public/js/stream-view.js +396 -55
- package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
- package/template/claude-task-manager/public/js/walle-session.js +234 -26
- package/template/claude-task-manager/public/js/walle.js +143 -2
- package/template/claude-task-manager/server.js +1402 -433
- package/template/claude-task-manager/session-integrity.js +77 -28
- package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
- package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
- package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent-runners/claude-code.js +2 -0
- package/template/wall-e/agent.js +63 -8
- package/template/wall-e/api-walle.js +330 -52
- package/template/wall-e/brain.js +291 -42
- package/template/wall-e/chat.js +172 -15
- package/template/wall-e/coding/compaction-service.js +19 -5
- package/template/wall-e/coding/stream-processor.js +22 -2
- package/template/wall-e/coding/workspace-replay.js +1 -4
- package/template/wall-e/coding-orchestrator.js +250 -80
- package/template/wall-e/compat.js +0 -28
- package/template/wall-e/context/context-builder.js +3 -1
- package/template/wall-e/embeddings.js +2 -7
- package/template/wall-e/eval/agent-runner.js +30 -9
- package/template/wall-e/eval/benchmark-generator.js +21 -1
- package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
- package/template/wall-e/eval/cc-replay.js +1 -0
- package/template/wall-e/eval/codex-cli-baseline.js +633 -0
- package/template/wall-e/eval/debug-agent003.js +1 -0
- package/template/wall-e/eval/eval-orchestrator.js +3 -3
- package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
- package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
- package/template/wall-e/eval/run-model-comparison.js +1 -0
- package/template/wall-e/eval/swebench-adapter.js +1 -0
- package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
- package/template/wall-e/extraction/knowledge-extractor.js +1 -2
- package/template/wall-e/lib/mcp-integration.js +336 -0
- package/template/wall-e/llm/ollama.js +47 -8
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/tool-adapter.js +1 -0
- package/template/wall-e/loops/ingest.js +42 -8
- package/template/wall-e/loops/initiative.js +87 -2
- package/template/wall-e/mcp-server.js +872 -19
- package/template/wall-e/memory/ctm-context-client.js +230 -0
- package/template/wall-e/memory/ctm-session-context.js +1376 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
- package/template/wall-e/server.js +30 -1
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
- package/template/wall-e/skills/skill-planner.js +86 -4
- package/template/wall-e/slack/socket-mode-listener.js +276 -0
- package/template/wall-e/telemetry.js +70 -2
- package/template/wall-e/tools/builtin-middleware.js +55 -2
- package/template/wall-e/tools/shell-policy.js +1 -1
- package/template/wall-e/tools/slack-owner.js +104 -0
- package/template/website/index.html +4 -4
- package/template/builder-journal.md +0 -17
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
const { execFile } = require('child_process');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
+
function _worktreeNamespace(options) {
|
|
6
|
+
const raw = typeof options === 'string'
|
|
7
|
+
? options
|
|
8
|
+
: (options && (options.namespace || options.agentType || options.owner)) || '';
|
|
9
|
+
const normalized = String(raw || '').toLowerCase();
|
|
10
|
+
return normalized === 'walle' || normalized === 'wall-e' ? 'walle' : 'claude';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function _worktreeParentDir(cwd, options) {
|
|
14
|
+
const namespace = _worktreeNamespace(options);
|
|
15
|
+
return path.join(cwd, namespace === 'walle' ? '.walle' : '.claude', 'worktrees');
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
// Run a git command in a given project directory
|
|
6
19
|
function git(cwd, args, maxBuffer = 1024 * 1024 * 5) {
|
|
7
20
|
return new Promise((resolve, reject) => {
|
|
@@ -301,10 +314,15 @@ async function _branchExists(cwd, branch) {
|
|
|
301
314
|
// worktree to it (no -b).
|
|
302
315
|
// - Path exists on disk but unregistered, or path/branch is in use by
|
|
303
316
|
// a different mapping → auto-suffix the name (-2, -3, …) up to -20.
|
|
304
|
-
async function createWorktree(cwd, name, baseBranch) {
|
|
317
|
+
async function createWorktree(cwd, name, baseBranch, options) {
|
|
305
318
|
const fs = require('fs');
|
|
319
|
+
if (baseBranch && typeof baseBranch === 'object') {
|
|
320
|
+
options = baseBranch;
|
|
321
|
+
baseBranch = undefined;
|
|
322
|
+
}
|
|
306
323
|
const base = baseBranch || 'HEAD';
|
|
307
|
-
const
|
|
324
|
+
const namespace = _worktreeNamespace(options);
|
|
325
|
+
const parentDir = _worktreeParentDir(cwd, namespace);
|
|
308
326
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
309
327
|
|
|
310
328
|
const worktrees = await listWorktrees(cwd).catch(() => []);
|
|
@@ -315,7 +333,7 @@ async function createWorktree(cwd, name, baseBranch) {
|
|
|
315
333
|
const initialReal = _real(initialPath);
|
|
316
334
|
const existingAtPath = worktrees.find(w => _real(w.path) === initialReal);
|
|
317
335
|
if (existingAtPath && existingAtPath.branch === name) {
|
|
318
|
-
return { path: existingAtPath.path, branch: existingAtPath.branch, reused: true };
|
|
336
|
+
return { path: existingAtPath.path, branch: existingAtPath.branch, reused: true, namespace };
|
|
319
337
|
}
|
|
320
338
|
|
|
321
339
|
// Attach to existing branch (no clash on path). Common after a worktree
|
|
@@ -323,7 +341,7 @@ async function createWorktree(cwd, name, baseBranch) {
|
|
|
323
341
|
if (!existingAtPath && !fs.existsSync(initialPath) && await _branchExists(cwd, name)
|
|
324
342
|
&& !worktrees.some(w => w.branch === name)) {
|
|
325
343
|
await git(cwd, ['worktree', 'add', initialPath, name]);
|
|
326
|
-
return { path: initialPath, branch: name, attached: true };
|
|
344
|
+
return { path: initialPath, branch: name, attached: true, namespace };
|
|
327
345
|
}
|
|
328
346
|
|
|
329
347
|
// Pick a free (path, branch) pair. Suffix until both slots are clear.
|
|
@@ -339,6 +357,7 @@ async function createWorktree(cwd, name, baseBranch) {
|
|
|
339
357
|
return {
|
|
340
358
|
path: candidatePath,
|
|
341
359
|
branch: candidateName,
|
|
360
|
+
namespace,
|
|
342
361
|
...(candidateName !== name ? { suffixed: true, requestedName: name } : {}),
|
|
343
362
|
};
|
|
344
363
|
}
|
|
@@ -437,7 +456,7 @@ function _realpath(p) {
|
|
|
437
456
|
//
|
|
438
457
|
// listRichWorktrees(cwd, mainBranch) returns each worktree augmented with:
|
|
439
458
|
// - state: 'primary'|'clean'|'ahead'|'behind'|'diverged'|'dirty'|'detached'|'ghost'
|
|
440
|
-
// - isCanonical: path lives under <repo>/.claude/worktrees/<name>
|
|
459
|
+
// - isCanonical: path lives under an agent-owned <repo>/.claude|.walle/worktrees/<name>
|
|
441
460
|
// - isGhost: path contains /~/ corruption OR path doesn't exist on disk
|
|
442
461
|
// - ahead/behind vs mainBranch
|
|
443
462
|
// - dirtyFiles: count of modified+untracked
|
|
@@ -457,6 +476,106 @@ async function _gitSafe(cwd, args, timeoutMs = 5000) {
|
|
|
457
476
|
});
|
|
458
477
|
}
|
|
459
478
|
|
|
479
|
+
function _parseGitHubRemoteUrl(remoteUrl) {
|
|
480
|
+
const raw = String(remoteUrl || '').trim();
|
|
481
|
+
if (!raw) return null;
|
|
482
|
+
|
|
483
|
+
let host = 'github.com';
|
|
484
|
+
let owner = '';
|
|
485
|
+
let repo = '';
|
|
486
|
+
let match = raw.match(/^git@([^:]+):([^/]+)\/(.+)$/);
|
|
487
|
+
if (match) {
|
|
488
|
+
host = match[1];
|
|
489
|
+
owner = match[2];
|
|
490
|
+
repo = match[3];
|
|
491
|
+
} else {
|
|
492
|
+
match = raw.match(/^ssh:\/\/(?:[^@/]+@)?([^/]+)\/([^/]+)\/(.+)$/);
|
|
493
|
+
if (match) {
|
|
494
|
+
host = match[1];
|
|
495
|
+
owner = match[2];
|
|
496
|
+
repo = match[3];
|
|
497
|
+
} else {
|
|
498
|
+
match = raw.match(/^https?:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
499
|
+
if (!match) return null;
|
|
500
|
+
host = match[1];
|
|
501
|
+
owner = match[2];
|
|
502
|
+
repo = match[3];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
repo = repo.replace(/(?:\.git)?\/?$/, '');
|
|
507
|
+
if (!host || !owner || !repo || repo.includes('/')) return null;
|
|
508
|
+
const nameWithOwner = `${owner}/${repo}`;
|
|
509
|
+
return {
|
|
510
|
+
host,
|
|
511
|
+
owner,
|
|
512
|
+
repo,
|
|
513
|
+
nameWithOwner,
|
|
514
|
+
ghRepo: host === 'github.com' ? nameWithOwner : `${host}/${nameWithOwner}`,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function _githubRemoteSpec(cwd, remoteName) {
|
|
519
|
+
if (!remoteName) return null;
|
|
520
|
+
const urls = [
|
|
521
|
+
await _gitSafe(cwd, ['remote', 'get-url', remoteName]),
|
|
522
|
+
await _gitSafe(cwd, ['remote', 'get-url', '--push', remoteName]),
|
|
523
|
+
].filter(Boolean);
|
|
524
|
+
for (const url of urls) {
|
|
525
|
+
const spec = _parseGitHubRemoteUrl(url);
|
|
526
|
+
if (spec) return { ...spec, remote: remoteName, url };
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function _selectPrBaseRemote(cwd, base, remoteNames) {
|
|
532
|
+
const remotes = Array.isArray(remoteNames) ? remoteNames : [];
|
|
533
|
+
if (remotes.includes('upstream') && await _githubRemoteSpec(cwd, 'upstream')) {
|
|
534
|
+
return 'upstream';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const configured = await _gitSafe(cwd, ['config', '--get', `branch.${base}.remote`]);
|
|
538
|
+
if (configured && configured !== '.' && remotes.includes(configured)
|
|
539
|
+
&& await _githubRemoteSpec(cwd, configured)) {
|
|
540
|
+
return configured;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (remotes.includes('origin') && await _githubRemoteSpec(cwd, 'origin')) {
|
|
544
|
+
return 'origin';
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
for (const remote of remotes) {
|
|
548
|
+
if (await _githubRemoteSpec(cwd, remote)) return remote;
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function _buildGhPrCreateArgs(cwd, branchName, opts) {
|
|
554
|
+
opts = opts || {};
|
|
555
|
+
const base = opts.base || 'main';
|
|
556
|
+
const title = opts.title || branchName;
|
|
557
|
+
const body = opts.body || `Branch \`${branchName}\` opened.`;
|
|
558
|
+
const remoteOut = await _gitSafe(cwd, ['remote']);
|
|
559
|
+
const remoteNames = remoteOut ? remoteOut.split('\n').map(s => s.trim()).filter(Boolean) : [];
|
|
560
|
+
const pushRemote = remoteNames.includes('origin') ? 'origin' : remoteNames[0];
|
|
561
|
+
const baseRemote = await _selectPrBaseRemote(cwd, base, remoteNames);
|
|
562
|
+
const baseSpec = await _githubRemoteSpec(cwd, baseRemote);
|
|
563
|
+
const pushSpec = await _githubRemoteSpec(cwd, pushRemote);
|
|
564
|
+
|
|
565
|
+
let head = branchName;
|
|
566
|
+
if (baseSpec && pushSpec
|
|
567
|
+
&& (baseSpec.host !== pushSpec.host
|
|
568
|
+
|| baseSpec.owner !== pushSpec.owner
|
|
569
|
+
|| baseSpec.repo !== pushSpec.repo)) {
|
|
570
|
+
head = `${pushSpec.owner}:${branchName}`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const args = ['pr', 'create'];
|
|
574
|
+
if (baseSpec) args.push('--repo', baseSpec.ghRepo);
|
|
575
|
+
args.push('--base', base, '--head', head, '--title', title, '--body', body);
|
|
576
|
+
return { args, baseRemote, pushRemote, baseSpec, pushSpec, head };
|
|
577
|
+
}
|
|
578
|
+
|
|
460
579
|
function _isGhostPath(p) {
|
|
461
580
|
if (!p) return true;
|
|
462
581
|
if (p.includes('/~/')) return true; // literal-tilde corruption
|
|
@@ -465,8 +584,10 @@ function _isGhostPath(p) {
|
|
|
465
584
|
|
|
466
585
|
function _isCanonicalPath(repoRoot, p, isMain) {
|
|
467
586
|
if (isMain) return true;
|
|
468
|
-
const
|
|
469
|
-
|
|
587
|
+
const value = String(p || '');
|
|
588
|
+
const expectedClaude = path.join(repoRoot, '.claude', 'worktrees') + path.sep;
|
|
589
|
+
const expectedWalle = path.join(repoRoot, '.walle', 'worktrees') + path.sep;
|
|
590
|
+
return value.startsWith(expectedClaude) || value.startsWith(expectedWalle);
|
|
470
591
|
}
|
|
471
592
|
|
|
472
593
|
function _classifyState(wt) {
|
|
@@ -949,11 +1070,18 @@ async function pushAndCreatePR(cwd, branchName, opts) {
|
|
|
949
1070
|
body = log ? `## Summary\n\n${log}\n` : `Branch \`${branchName}\` opened.`;
|
|
950
1071
|
}
|
|
951
1072
|
|
|
952
|
-
// gh pr create
|
|
1073
|
+
// gh pr create. Do not let gh infer the repository from `origin`: in forked
|
|
1074
|
+
// or stale-remote checkouts that can point it at an inaccessible repo.
|
|
1075
|
+
const prCommand = await _buildGhPrCreateArgs(wt.path, branchName, { base, title, body });
|
|
953
1076
|
const ghOut = await new Promise((resolve, reject) => {
|
|
954
|
-
execFile('gh',
|
|
1077
|
+
execFile('gh', prCommand.args,
|
|
955
1078
|
{ cwd: wt.path, timeout: 60000 }, (err, stdout, stderr) => {
|
|
956
|
-
if (err)
|
|
1079
|
+
if (err) {
|
|
1080
|
+
const target = prCommand.baseSpec
|
|
1081
|
+
? `${prCommand.baseSpec.ghRepo} from ${prCommand.head}`
|
|
1082
|
+
: `the repository inferred by gh from ${wt.path}`;
|
|
1083
|
+
return reject(new Error(`gh pr create failed for ${target}: ${stderr || err.message}`));
|
|
1084
|
+
}
|
|
957
1085
|
resolve((stdout || '').trim());
|
|
958
1086
|
});
|
|
959
1087
|
});
|
|
@@ -971,4 +1099,6 @@ module.exports = {
|
|
|
971
1099
|
// Internal helpers exposed for tests
|
|
972
1100
|
_classifyState, _buildSummary, _isGhostPath, _isCanonicalPath, STATE_SORT_RANK,
|
|
973
1101
|
_classifyMainRemote, _recommendedAction, _syncAllEligibility, _sameWorktreePath,
|
|
1102
|
+
_parseGitHubRemoteUrl, _githubRemoteSpec, _buildGhPrCreateArgs,
|
|
1103
|
+
_worktreeNamespace, _worktreeParentDir,
|
|
974
1104
|
};
|
|
@@ -101,6 +101,35 @@ function getPreset(presetId) {
|
|
|
101
101
|
return PRESETS[presetId] || null;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
const INHERITED_AGENT_ENV_KEYS = [
|
|
105
|
+
'CLAUDECODE',
|
|
106
|
+
'CLAUDE_CODE',
|
|
107
|
+
'CLAUDE_CODE_ENTRYPOINT',
|
|
108
|
+
'CLAUDE_CODE_ENABLE_TELEMETRY',
|
|
109
|
+
'CODEX_THREAD_ID',
|
|
110
|
+
'CODEX_SESSION_ID',
|
|
111
|
+
'CODEX_TURN_ID',
|
|
112
|
+
'CODEX_CI',
|
|
113
|
+
'CODEX_SANDBOX_NETWORK_DISABLED',
|
|
114
|
+
'GEMINI_SESSION_ID',
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
function sanitizeInheritedAgentEnv(env) {
|
|
118
|
+
if (!env || typeof env !== 'object') return env;
|
|
119
|
+
for (const key of INHERITED_AGENT_ENV_KEYS) delete env[key];
|
|
120
|
+
return env;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildResourceAttributes(existing, ctmSessionId) {
|
|
124
|
+
const prior = String(existing || '')
|
|
125
|
+
.split(',')
|
|
126
|
+
.map(s => s.trim())
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.filter(s => !/^ctm\.session_id=/.test(s) && !/^clideck\.session_id=/.test(s));
|
|
129
|
+
prior.push(`ctm.session_id=${ctmSessionId}`);
|
|
130
|
+
return prior.join(',');
|
|
131
|
+
}
|
|
132
|
+
|
|
104
133
|
/**
|
|
105
134
|
* Build the env object to inject at pty.spawn time.
|
|
106
135
|
* - ctmSessionId: CTM's UUID for the session (goes into OTEL_RESOURCE_ATTRIBUTES)
|
|
@@ -109,21 +138,25 @@ function getPreset(presetId) {
|
|
|
109
138
|
* Returns an object that can be spread into the env option: `env: { ...process.env, ...telemetryEnv }`.
|
|
110
139
|
* Always includes CTM_SESSION_ID and CTM_PORT — hook bins read these from their own env.
|
|
111
140
|
*/
|
|
112
|
-
function buildTelemetryEnv(ctmSessionId, cmd, port) {
|
|
141
|
+
function buildTelemetryEnv(ctmSessionId, cmd, port, baseEnv = process.env) {
|
|
113
142
|
const presetId = detectPresetId(cmd);
|
|
114
143
|
const preset = presetId ? PRESETS[presetId] : null;
|
|
115
144
|
const env = {
|
|
116
145
|
CTM_SESSION_ID: ctmSessionId,
|
|
117
146
|
CTM_PORT: String(port),
|
|
147
|
+
OTEL_RESOURCE_ATTRIBUTES: buildResourceAttributes(baseEnv?.OTEL_RESOURCE_ATTRIBUTES, ctmSessionId),
|
|
118
148
|
};
|
|
119
149
|
if (!preset || !preset.telemetryEnv) return env;
|
|
120
150
|
for (const [k, v] of Object.entries(preset.telemetryEnv)) {
|
|
121
151
|
env[k] = String(v).replace('{{port}}', String(port));
|
|
122
152
|
}
|
|
123
|
-
// Correlate OTLP log records to the CTM session id
|
|
124
|
-
const existing = process.env.OTEL_RESOURCE_ATTRIBUTES || '';
|
|
125
|
-
env.OTEL_RESOURCE_ATTRIBUTES = (existing ? existing + ',' : '') + `ctm.session_id=${ctmSessionId}`;
|
|
126
153
|
return env;
|
|
127
154
|
}
|
|
128
155
|
|
|
129
|
-
module.exports = {
|
|
156
|
+
module.exports = {
|
|
157
|
+
PRESETS,
|
|
158
|
+
detectPresetId,
|
|
159
|
+
getPreset,
|
|
160
|
+
buildTelemetryEnv,
|
|
161
|
+
sanitizeInheritedAgentEnv,
|
|
162
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
function stripAnsi(text) {
|
|
6
|
+
return String(text || '')
|
|
7
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
8
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeForTerminalSearch(text) {
|
|
12
|
+
return stripAnsi(text)
|
|
13
|
+
.normalize('NFKC')
|
|
14
|
+
.replace(/[*_`#>~]/g, '')
|
|
15
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
16
|
+
.replace(/\s+/g, ' ')
|
|
17
|
+
.trim()
|
|
18
|
+
.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function meaningfulLines(text) {
|
|
22
|
+
return stripAnsi(text)
|
|
23
|
+
.split(/\r?\n/)
|
|
24
|
+
.map((line) => line.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function markerForFinalText(text) {
|
|
29
|
+
const lines = meaningfulLines(text);
|
|
30
|
+
if (lines.length === 0) return '';
|
|
31
|
+
let candidate = lines.find((line) => normalizeForTerminalSearch(line).length >= 24) || lines[0];
|
|
32
|
+
if (normalizeForTerminalSearch(candidate).length < 24 && lines.length > 1) {
|
|
33
|
+
candidate = `${candidate} ${lines[1]}`;
|
|
34
|
+
}
|
|
35
|
+
return normalizeForTerminalSearch(candidate).slice(0, 180);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function terminalContainsFinalText(terminalText, finalText) {
|
|
39
|
+
const marker = markerForFinalText(finalText);
|
|
40
|
+
if (!marker) return true;
|
|
41
|
+
return normalizeForTerminalSearch(terminalText).includes(marker);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fingerprintFinalText(text) {
|
|
45
|
+
return crypto.createHash('sha1').update(String(text || '')).digest('hex').slice(0, 16);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
fingerprintFinalText,
|
|
50
|
+
markerForFinalText,
|
|
51
|
+
normalizeForTerminalSearch,
|
|
52
|
+
terminalContainsFinalText,
|
|
53
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
let ctmSessionContext = null;
|
|
4
|
+
let ctmSessionContextLoadError = null;
|
|
5
|
+
|
|
6
|
+
function loadCtmSessionContext() {
|
|
7
|
+
if (ctmSessionContext || ctmSessionContextLoadError) return ctmSessionContext;
|
|
8
|
+
try {
|
|
9
|
+
ctmSessionContext = require('../../wall-e/memory/ctm-session-context');
|
|
10
|
+
} catch (err) {
|
|
11
|
+
ctmSessionContextLoadError = err;
|
|
12
|
+
}
|
|
13
|
+
return ctmSessionContext;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function handleCtmSessionContextApi(req, res, url, { db, dbModule } = {}) {
|
|
17
|
+
if (!url.pathname.startsWith('/api/ctm/session-memory')) return false;
|
|
18
|
+
|
|
19
|
+
const context = loadCtmSessionContext();
|
|
20
|
+
if (!context) {
|
|
21
|
+
return respondJson(res, 503, {
|
|
22
|
+
ok: false,
|
|
23
|
+
source: 'ctm-api',
|
|
24
|
+
reason: 'ctm_session_context_unavailable',
|
|
25
|
+
error: ctmSessionContextLoadError?.message || 'CTM session context module unavailable',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
if (url.pathname === '/api/ctm/session-memory/health' && req.method === 'GET') {
|
|
31
|
+
return respondJson(res, 200, asApiResult(context.getCtmDbHealth({ db })));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (url.pathname === '/api/ctm/session-memory/search' && req.method === 'GET') {
|
|
35
|
+
const query = url.searchParams.get('q') || url.searchParams.get('query') || '';
|
|
36
|
+
const limit = url.searchParams.get('limit') || undefined;
|
|
37
|
+
return respondJson(res, 200, asApiResult(context.searchCtmSessions({ query, limit, db })));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (url.pathname === '/api/ctm/session-memory/context' && req.method === 'GET') {
|
|
41
|
+
return respondJson(res, 200, asApiResult(context.getCtmSessionContext({
|
|
42
|
+
session_id: url.searchParams.get('session_id') || url.searchParams.get('id') || '',
|
|
43
|
+
session_ids: parseListParams(url.searchParams, 'session_id', 'session_ids', 'ids'),
|
|
44
|
+
limit: url.searchParams.get('limit') || undefined,
|
|
45
|
+
cursor: url.searchParams.get('cursor') || undefined,
|
|
46
|
+
include_raw: parseBool(url.searchParams.get('include_raw')),
|
|
47
|
+
dedupe: !parseBool(url.searchParams.get('no_dedupe')),
|
|
48
|
+
format: url.searchParams.get('format') || 'messages',
|
|
49
|
+
db,
|
|
50
|
+
})));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
(url.pathname === '/api/ctm/session-memory/message-index/backfill'
|
|
55
|
+
|| url.pathname === '/api/ctm/session-memory/rebuild-message-index')
|
|
56
|
+
&& req.method === 'POST'
|
|
57
|
+
) {
|
|
58
|
+
readJsonBody(req, 32 * 1024)
|
|
59
|
+
.then((body) => handleMessageIndexBackfill({ body, res, context, db, dbModule })
|
|
60
|
+
.catch((err) => respondJson(res, 500, {
|
|
61
|
+
ok: false,
|
|
62
|
+
source: 'ctm-api',
|
|
63
|
+
authority: 'ctm',
|
|
64
|
+
reason: 'ctm_message_index_backfill_failed',
|
|
65
|
+
error: err.message,
|
|
66
|
+
})))
|
|
67
|
+
.catch((err) => respondJson(res, 400, {
|
|
68
|
+
ok: false,
|
|
69
|
+
source: 'ctm-api',
|
|
70
|
+
reason: 'invalid_json',
|
|
71
|
+
error: err.message,
|
|
72
|
+
}));
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (url.pathname === '/api/ctm/session-memory/context-pack' && req.method === 'POST') {
|
|
77
|
+
readJsonBody(req, 128 * 1024)
|
|
78
|
+
.then((body) => {
|
|
79
|
+
respondJson(res, 200, asApiResult(context.buildContextPack({
|
|
80
|
+
task: body.task || '',
|
|
81
|
+
query: body.query || '',
|
|
82
|
+
session_ids: body.session_ids || body.ids,
|
|
83
|
+
limit: body.limit,
|
|
84
|
+
token_budget: body.token_budget,
|
|
85
|
+
include_raw: Boolean(body.include_raw),
|
|
86
|
+
mode: body.mode || 'auto',
|
|
87
|
+
db,
|
|
88
|
+
})));
|
|
89
|
+
})
|
|
90
|
+
.catch((err) => respondJson(res, 400, {
|
|
91
|
+
ok: false,
|
|
92
|
+
source: 'ctm-api',
|
|
93
|
+
reason: 'invalid_json',
|
|
94
|
+
error: err.message,
|
|
95
|
+
}));
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return respondJson(res, 404, {
|
|
100
|
+
ok: false,
|
|
101
|
+
source: 'ctm-api',
|
|
102
|
+
reason: 'not_found',
|
|
103
|
+
});
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return respondJson(res, 500, {
|
|
106
|
+
ok: false,
|
|
107
|
+
source: 'ctm-api',
|
|
108
|
+
reason: 'ctm_session_context_failed',
|
|
109
|
+
error: err.message,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function handleMessageIndexBackfill({ body, res, context, db, dbModule }) {
|
|
115
|
+
const limit = clampInt(body.limit, 100, 1, 1000);
|
|
116
|
+
const chunkSize = clampInt(body.chunk_size ?? body.chunkSize, 250, 25, 2000);
|
|
117
|
+
const dryRun = parseBool(body.dry_run);
|
|
118
|
+
if (dryRun) {
|
|
119
|
+
return respondJson(res, 200, asApiResult(context.backfillCtmSessionMessages({
|
|
120
|
+
db,
|
|
121
|
+
limit,
|
|
122
|
+
dry_run: true,
|
|
123
|
+
})));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!dbModule || typeof dbModule.backfillSessionMessagesAsync !== 'function') {
|
|
127
|
+
return respondJson(res, 503, {
|
|
128
|
+
ok: false,
|
|
129
|
+
source: 'ctm-api',
|
|
130
|
+
authority: 'ctm',
|
|
131
|
+
reason: 'ctm_message_index_writer_unavailable',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const insertedMessages = await dbModule.backfillSessionMessagesAsync({ limit, chunkSize });
|
|
136
|
+
return respondJson(res, 200, asApiResult({
|
|
137
|
+
ok: true,
|
|
138
|
+
source: 'ctm-db',
|
|
139
|
+
dry_run: false,
|
|
140
|
+
limit,
|
|
141
|
+
chunk_size: chunkSize,
|
|
142
|
+
inserted_messages: insertedMessages,
|
|
143
|
+
table_counts: tableCounts(db, ['session_conversations', 'session_messages', 'session_messages_fts']),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function asApiResult(result) {
|
|
148
|
+
return {
|
|
149
|
+
...(result || {}),
|
|
150
|
+
source: 'ctm-api',
|
|
151
|
+
backend_source: result?.source || 'ctm-db',
|
|
152
|
+
authority: 'ctm',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseListParams(searchParams, ...names) {
|
|
157
|
+
const values = [];
|
|
158
|
+
for (const name of names) {
|
|
159
|
+
for (const value of searchParams.getAll(name)) {
|
|
160
|
+
values.push(...String(value || '').split(','));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return values.map((value) => value.trim()).filter(Boolean);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseBool(value) {
|
|
167
|
+
return value === true || value === 1 || value === '1' || value === 'true' || value === 'yes';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function clampInt(value, fallback, min, max) {
|
|
171
|
+
const n = Number(value);
|
|
172
|
+
if (!Number.isFinite(n)) return fallback;
|
|
173
|
+
return Math.min(Math.max(Math.trunc(n), min), max);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function tableCounts(db, names) {
|
|
177
|
+
const counts = {};
|
|
178
|
+
if (!db) return counts;
|
|
179
|
+
for (const name of names) {
|
|
180
|
+
try {
|
|
181
|
+
const exists = db.prepare("SELECT 1 FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?").get(name);
|
|
182
|
+
if (!exists) continue;
|
|
183
|
+
counts[name] = db.prepare(`SELECT COUNT(*) AS count FROM ${name}`).get().count;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
counts._errors = counts._errors || {};
|
|
186
|
+
counts._errors[name] = err.message;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return counts;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function readJsonBody(req, maxBytes) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
let body = '';
|
|
195
|
+
req.on('data', (chunk) => {
|
|
196
|
+
body += chunk;
|
|
197
|
+
if (body.length > maxBytes) {
|
|
198
|
+
reject(new Error('request_body_too_large'));
|
|
199
|
+
req.destroy();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
req.on('end', () => {
|
|
203
|
+
try {
|
|
204
|
+
resolve(body ? JSON.parse(body) : {});
|
|
205
|
+
} catch (err) {
|
|
206
|
+
reject(err);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
req.on('error', reject);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function respondJson(res, status, payload) {
|
|
214
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
215
|
+
res.end(JSON.stringify(payload));
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
handleCtmSessionContextApi,
|
|
221
|
+
loadCtmSessionContext,
|
|
222
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createSessionDiagnostics({ limit = 80, clock = () => Date.now() } = {}) {
|
|
4
|
+
const rings = new Map();
|
|
5
|
+
const cap = Math.max(1, Math.min(Number(limit) || 80, 500));
|
|
6
|
+
|
|
7
|
+
function record(sessionId, event, details = {}) {
|
|
8
|
+
const id = String(sessionId || '').trim();
|
|
9
|
+
if (!id) return null;
|
|
10
|
+
const entry = {
|
|
11
|
+
ts: clock(),
|
|
12
|
+
event: String(event || 'event'),
|
|
13
|
+
...sanitizeDetails(details),
|
|
14
|
+
};
|
|
15
|
+
const ring = rings.get(id) || [];
|
|
16
|
+
ring.push(entry);
|
|
17
|
+
while (ring.length > cap) ring.shift();
|
|
18
|
+
rings.set(id, ring);
|
|
19
|
+
return entry;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function get(sessionId) {
|
|
23
|
+
const id = String(sessionId || '').trim();
|
|
24
|
+
return [...(rings.get(id) || [])];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function clear(sessionId) {
|
|
28
|
+
rings.delete(String(sessionId || '').trim());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { record, get, clear };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sanitizeDetails(details) {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [key, value] of Object.entries(details || {})) {
|
|
37
|
+
if (value == null) continue;
|
|
38
|
+
if (typeof value === 'string') {
|
|
39
|
+
out[key] = value.length > 500 ? `${value.slice(0, 497)}...` : value;
|
|
40
|
+
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
41
|
+
out[key] = value;
|
|
42
|
+
} else if (Array.isArray(value)) {
|
|
43
|
+
out[key] = value.slice(0, 20).map((item) => (
|
|
44
|
+
typeof item === 'string' && item.length > 200 ? `${item.slice(0, 197)}...` : item
|
|
45
|
+
));
|
|
46
|
+
} else if (typeof value === 'object') {
|
|
47
|
+
out[key] = sanitizeDetails(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
createSessionDiagnostics,
|
|
55
|
+
sanitizeDetails,
|
|
56
|
+
};
|