thoth-agents 0.2.11 → 0.3.1
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/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +21 -0
- package/README.md +301 -311
- package/dist/agents/configured-role-prompt.d.ts +9 -0
- package/dist/agents/prompt-sections.d.ts +8 -4
- package/dist/agents/prompt-utils.d.ts +0 -4
- package/dist/{chunk-ASKXWU6E.js → chunk-2MZ5C2RN.js} +1078 -1725
- package/dist/chunk-XO74DXBZ.js +1781 -0
- package/dist/{chunk-6BNAV534.js → chunk-YUCGIJPP.js} +12 -30
- package/dist/cli/claude-code-install.d.ts +24 -26
- package/dist/cli/claude-code-paths.d.ts +0 -28
- package/dist/cli/codex-install.d.ts +2 -2
- package/dist/cli/codex-paths.d.ts +0 -2
- package/dist/cli/index.js +279 -119
- package/dist/cli/install.d.ts +5 -1
- package/dist/cli/model-catalog/cache.d.ts +1 -1
- package/dist/cli/operations/claude-code.d.ts +6 -3
- package/dist/cli/operations/codex.d.ts +2 -1
- package/dist/cli/operations/opencode.d.ts +2 -1
- package/dist/cli/operations/types.d.ts +9 -1
- package/dist/cli/package-root.d.ts +1 -0
- package/dist/cli/providers.d.ts +6 -116
- package/dist/cli/skills.d.ts +19 -32
- package/dist/cli/thoth-mem-install.d.ts +45 -0
- package/dist/cli/tui/components/StatusView.d.ts +12 -1
- package/dist/cli/tui/index.js +256 -142
- package/dist/cli/tui/operations.d.ts +4 -3
- package/dist/cli/types.d.ts +0 -10
- package/dist/config/constants.d.ts +20 -16
- package/dist/config/index.d.ts +0 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/schema.d.ts +2 -38
- package/dist/config/utils.d.ts +1 -2
- package/dist/harness/adapters/codex-surfaces.d.ts +3 -3
- package/dist/harness/core/agent-pack.d.ts +37 -29
- package/dist/harness/core/memory-governance.d.ts +24 -48
- package/dist/harness/core/sdd.d.ts +364 -131
- package/dist/harness/generate-integration-packages.d.ts +20 -0
- package/dist/harness/opencode-init-command.d.ts +11 -0
- package/dist/harness/types.d.ts +14 -1
- package/dist/harness/writers/claude-code-subagent.d.ts +3 -1
- package/dist/hooks/index.d.ts +0 -5
- package/dist/index.js +562 -1314
- package/dist/mcp/index.d.ts +1 -2
- package/package.json +17 -12
- package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +50 -0
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/.codex-plugin/.thoth-agents-plugin-assets.json +16 -0
- package/plugin/.codex-plugin/plugin.json +7 -0
- package/plugin/.mcp.json +19 -0
- package/plugin/agents/deep.md +89 -0
- package/plugin/agents/designer.md +89 -0
- package/plugin/agents/explorer.md +90 -0
- package/plugin/agents/librarian.md +90 -0
- package/plugin/agents/oracle.md +92 -0
- package/plugin/agents/orchestrator.md +128 -0
- package/plugin/agents/quick.md +89 -0
- package/plugin/codex.mcp.json +17 -0
- package/plugin/settings.json +3 -0
- package/plugin/skills/thoth-archive/SKILL.md +40 -0
- package/plugin/skills/thoth-archive/scripts/archive.mjs +748 -0
- package/plugin/skills/thoth-constitution/SKILL.md +45 -0
- package/plugin/skills/thoth-constitution/scripts/validate.mjs +167 -0
- package/plugin/skills/thoth-constitution/templates/constitution.md +54 -0
- package/plugin/skills/thoth-init/SKILL.md +30 -0
- package/plugin/skills/thoth-init/scripts/init.mjs +141 -0
- package/plugin/skills/thoth-sdd/SKILL.md +83 -0
- package/plugin/skills/thoth-sdd/references/phases/analyze.md +21 -0
- package/plugin/skills/thoth-sdd/references/phases/checklist.md +20 -0
- package/plugin/skills/thoth-sdd/references/phases/clarify.md +15 -0
- package/plugin/skills/thoth-sdd/references/phases/converge.md +14 -0
- package/plugin/skills/thoth-sdd/references/phases/explore.md +10 -0
- package/plugin/skills/thoth-sdd/references/phases/implement.md +19 -0
- package/plugin/skills/thoth-sdd/references/phases/plan.md +15 -0
- package/plugin/skills/thoth-sdd/references/phases/specify.md +21 -0
- package/plugin/skills/thoth-sdd/references/phases/tasks.md +20 -0
- package/plugin/skills/thoth-sdd/references/phases/verify.md +22 -0
- package/plugin/skills/thoth-sdd/scripts/validate.mjs +1117 -0
- package/plugin/skills/thoth-sdd/templates/archive-report.md +25 -0
- package/plugin/skills/thoth-sdd/templates/checklist.md +27 -0
- package/plugin/skills/thoth-sdd/templates/plan.md +38 -0
- package/plugin/skills/thoth-sdd/templates/spec.md +54 -0
- package/plugin/skills/thoth-sdd/templates/tasks.md +29 -0
- package/plugin/skills/thoth-sdd/templates/verify-report.md +28 -0
- package/skills/README.md +12 -0
- package/skills/thoth-archive/SKILL.md +40 -0
- package/skills/thoth-archive/scripts/archive.mjs +748 -0
- package/skills/thoth-constitution/SKILL.md +45 -0
- package/skills/thoth-constitution/scripts/validate.mjs +167 -0
- package/skills/thoth-constitution/templates/constitution.md +54 -0
- package/skills/thoth-init/SKILL.md +30 -0
- package/skills/thoth-init/scripts/init.mjs +141 -0
- package/skills/thoth-sdd/SKILL.md +83 -0
- package/skills/thoth-sdd/references/phases/analyze.md +21 -0
- package/skills/thoth-sdd/references/phases/checklist.md +20 -0
- package/skills/thoth-sdd/references/phases/clarify.md +15 -0
- package/skills/thoth-sdd/references/phases/converge.md +14 -0
- package/skills/thoth-sdd/references/phases/explore.md +10 -0
- package/skills/thoth-sdd/references/phases/implement.md +19 -0
- package/skills/thoth-sdd/references/phases/plan.md +15 -0
- package/skills/thoth-sdd/references/phases/specify.md +21 -0
- package/skills/thoth-sdd/references/phases/tasks.md +20 -0
- package/skills/thoth-sdd/references/phases/verify.md +22 -0
- package/skills/thoth-sdd/scripts/validate.mjs +1117 -0
- package/skills/thoth-sdd/templates/archive-report.md +25 -0
- package/skills/thoth-sdd/templates/checklist.md +27 -0
- package/skills/thoth-sdd/templates/plan.md +38 -0
- package/skills/thoth-sdd/templates/spec.md +54 -0
- package/skills/thoth-sdd/templates/tasks.md +29 -0
- package/skills/thoth-sdd/templates/verify-report.md +28 -0
- package/thoth-agents.schema.json +0 -39
- package/dist/chunk-OZE7JVGS.js +0 -2344
- package/dist/cli/custom-skills.d.ts +0 -49
- package/dist/cli/skill-manifest.d.ts +0 -32
- package/dist/harness/core/skills.d.ts +0 -211
- package/dist/harness/generate-codex-plugin.d.ts +0 -9
- package/dist/harness/writers/claude-code-skill-layout.d.ts +0 -16
- package/dist/harness/writers/fs-skill-collect.d.ts +0 -7
- package/dist/harness/writers/skill-layout.d.ts +0 -16
- package/dist/hooks/chat-headers.d.ts +0 -16
- package/dist/hooks/phase-reminder/index.d.ts +0 -28
- package/dist/hooks/post-read-nudge/index.d.ts +0 -18
- package/dist/hooks/skill-sync.d.ts +0 -10
- package/dist/hooks/thoth-mem/index.d.ts +0 -46
- package/dist/hooks/thoth-mem/protocol.d.ts +0 -7
- package/dist/mcp/thoth.d.ts +0 -3
- package/dist/sdd/artifact-governance/artifact-loader.d.ts +0 -130
- package/dist/sdd/artifact-governance/index.d.ts +0 -6
- package/dist/sdd/artifact-governance/tasks-validator.d.ts +0 -17
- package/dist/sdd/artifact-governance/types.d.ts +0 -52
- package/dist/thoth/client.d.ts +0 -14
- package/dist/thoth/index.d.ts +0 -2
- package/src/skills/_shared/openspec-convention.md +0 -350
- package/src/skills/_shared/persistence-contract.md +0 -163
- package/src/skills/_shared/thoth-mem-convention.md +0 -123
- package/src/skills/executing-plans/SKILL.md +0 -288
- package/src/skills/plan-reviewer/SKILL.md +0 -219
- package/src/skills/requirements-interview/SKILL.md +0 -233
- package/src/skills/sdd-apply/SKILL.md +0 -89
- package/src/skills/sdd-archive/SKILL.md +0 -92
- package/src/skills/sdd-clarify/SKILL.md +0 -105
- package/src/skills/sdd-constitution/SKILL.md +0 -107
- package/src/skills/sdd-design/SKILL.md +0 -155
- package/src/skills/sdd-init/SKILL.md +0 -233
- package/src/skills/sdd-propose/SKILL.md +0 -101
- package/src/skills/sdd-spec/SKILL.md +0 -136
- package/src/skills/sdd-tasks/SKILL.md +0 -232
- package/src/skills/sdd-verify/SKILL.md +0 -131
- package/src/skills/thoth-mem-agents/SKILL.md +0 -210
package/dist/index.js
CHANGED
|
@@ -3,30 +3,66 @@ import {
|
|
|
3
3
|
renderOpenCodeAgentConfigs,
|
|
4
4
|
spawn,
|
|
5
5
|
spawnSync
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-YUCGIJPP.js";
|
|
7
7
|
import {
|
|
8
|
-
DEFAULT_THOTH_COMMAND,
|
|
9
8
|
POLL_INTERVAL_BACKGROUND_MS,
|
|
10
9
|
context7,
|
|
11
10
|
exa,
|
|
12
11
|
getOpenCodeConfigPaths,
|
|
13
12
|
grep_app,
|
|
14
|
-
installCustomSkills,
|
|
15
13
|
loadPluginConfig,
|
|
16
14
|
stripJsonComments
|
|
17
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-XO74DXBZ.js";
|
|
18
16
|
|
|
19
17
|
// src/index.ts
|
|
20
|
-
import
|
|
18
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
19
|
+
|
|
20
|
+
// src/harness/opencode-init-command.ts
|
|
21
|
+
import { existsSync, readFileSync } from "fs";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
function createOpenCodeInitCommand({
|
|
24
|
+
projectRoot,
|
|
25
|
+
packageRoot
|
|
26
|
+
}) {
|
|
27
|
+
const skillPath = join(packageRoot, "skills", "thoth-init", "SKILL.md");
|
|
28
|
+
const scriptPath = join(
|
|
29
|
+
packageRoot,
|
|
30
|
+
"skills",
|
|
31
|
+
"thoth-init",
|
|
32
|
+
"scripts",
|
|
33
|
+
"init.mjs"
|
|
34
|
+
);
|
|
35
|
+
if (!existsSync(skillPath) || !existsSync(scriptPath)) {
|
|
36
|
+
throw new Error("The bundled thoth-init skill is incomplete.");
|
|
37
|
+
}
|
|
38
|
+
const contract = readFileSync(skillPath, "utf8");
|
|
39
|
+
return {
|
|
40
|
+
description: "Initialize thoth-agents project SDD governance",
|
|
41
|
+
agent: "orchestrator",
|
|
42
|
+
subtask: false,
|
|
43
|
+
template: `Initialize this project now using the bundled thoth-init skill.
|
|
44
|
+
|
|
45
|
+
The operation must stay project-scoped, idempotent, and offline. Run:
|
|
46
|
+
|
|
47
|
+
node "${scriptPath}" --project "${projectRoot}" --harness opencode --json
|
|
48
|
+
|
|
49
|
+
Inspect the JSON result, report created and preserved files, and do not download
|
|
50
|
+
skills or invoke the thoth-agents CLI.
|
|
51
|
+
|
|
52
|
+
Bundled skill contract:
|
|
53
|
+
|
|
54
|
+
${contract}`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
21
57
|
|
|
22
58
|
// src/utils/logger.ts
|
|
23
59
|
import { createRequire } from "module";
|
|
24
60
|
var require2 = createRequire(import.meta.url);
|
|
25
61
|
function getLogFile() {
|
|
26
62
|
const os2 = require2("node:os");
|
|
27
|
-
const
|
|
63
|
+
const path4 = require2("node:path");
|
|
28
64
|
const configuredPath = process.env.THOTH_AGENTS_LOG_FILE?.trim();
|
|
29
|
-
return configuredPath ||
|
|
65
|
+
return configuredPath || path4.join(os2.tmpdir(), "thoth-agents.log");
|
|
30
66
|
}
|
|
31
67
|
function log(message, data) {
|
|
32
68
|
try {
|
|
@@ -465,557 +501,154 @@ function showToast(ctx, title, message, variant = "info", duration = 3e3) {
|
|
|
465
501
|
});
|
|
466
502
|
}
|
|
467
503
|
|
|
468
|
-
// src/
|
|
469
|
-
var
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
504
|
+
// src/hooks/delegate-task-retry/patterns.ts
|
|
505
|
+
var DELEGATE_TASK_ERROR_PATTERNS = [
|
|
506
|
+
{
|
|
507
|
+
pattern: "load_skills",
|
|
508
|
+
errorType: "missing_load_skills",
|
|
509
|
+
fixHint: "Add load_skills=[] (empty array when no skill is needed)."
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
pattern: "category OR subagent_type",
|
|
513
|
+
errorType: "mutual_exclusion",
|
|
514
|
+
fixHint: 'Provide only one: category (e.g., "unspecified-low") OR subagent_type (e.g., "explorer").'
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
pattern: "Must provide either category or subagent_type",
|
|
518
|
+
errorType: "missing_category_or_agent",
|
|
519
|
+
fixHint: 'Add either category="unspecified-low" or subagent_type="explorer".'
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
pattern: "Unknown category",
|
|
523
|
+
errorType: "unknown_category",
|
|
524
|
+
fixHint: "Use a valid category listed in the error output."
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
pattern: "Unknown agent",
|
|
528
|
+
errorType: "unknown_agent",
|
|
529
|
+
fixHint: "Use a valid agent name from the available list."
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
pattern: "Skills not found",
|
|
533
|
+
errorType: "unknown_skills",
|
|
534
|
+
fixHint: "Use valid skill names listed in the error output."
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
pattern: "is not allowed. Allowed agents:",
|
|
538
|
+
errorType: "background_agent_not_allowed",
|
|
539
|
+
fixHint: "Use one of the allowed agents shown in the error or delegate from a parent agent that can call this subagent."
|
|
476
540
|
}
|
|
477
|
-
|
|
478
|
-
|
|
541
|
+
];
|
|
542
|
+
function detectDelegateTaskError(output) {
|
|
543
|
+
if (!output || typeof output !== "string") return null;
|
|
544
|
+
const hasErrorSignal = output.includes("[ERROR]") || output.includes("Invalid arguments") || output.includes("is not allowed. Allowed agents:");
|
|
545
|
+
if (!hasErrorSignal) return null;
|
|
546
|
+
for (const pattern of DELEGATE_TASK_ERROR_PATTERNS) {
|
|
547
|
+
if (output.includes(pattern.pattern)) {
|
|
548
|
+
return {
|
|
549
|
+
errorType: pattern.errorType,
|
|
550
|
+
originalOutput: output
|
|
551
|
+
};
|
|
552
|
+
}
|
|
479
553
|
}
|
|
480
|
-
return
|
|
554
|
+
return null;
|
|
481
555
|
}
|
|
482
556
|
|
|
483
|
-
// src/
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
557
|
+
// src/hooks/delegate-task-retry/guidance.ts
|
|
558
|
+
function extractAvailableList(output) {
|
|
559
|
+
const match = output.match(/Allowed agents:\s*(.+)$/m);
|
|
560
|
+
if (match) return match[1].trim();
|
|
561
|
+
const available = output.match(/Available[^:]*:\s*(.+)$/m);
|
|
562
|
+
if (available) return available[1].trim();
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
function buildRetryGuidance(errorInfo) {
|
|
566
|
+
const pattern = DELEGATE_TASK_ERROR_PATTERNS.find(
|
|
567
|
+
(p) => p.errorType === errorInfo.errorType
|
|
568
|
+
);
|
|
569
|
+
if (!pattern) {
|
|
570
|
+
return "\n[delegate-task retry] Fix parameters and retry with corrected arguments.";
|
|
492
571
|
}
|
|
493
|
-
const
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
() => null
|
|
503
|
-
);
|
|
504
|
-
} finally {
|
|
505
|
-
clearTimeout(timeout);
|
|
506
|
-
}
|
|
507
|
-
const available = response?.ok ?? false;
|
|
508
|
-
if (available) {
|
|
509
|
-
serverCheckUrl = serverUrl;
|
|
510
|
-
serverAvailable = true;
|
|
511
|
-
log("[tmux] isServerRunning: checked", { serverUrl, available, attempt });
|
|
512
|
-
return true;
|
|
513
|
-
}
|
|
514
|
-
if (attempt < maxAttempts) {
|
|
515
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
516
|
-
}
|
|
572
|
+
const available = extractAvailableList(errorInfo.originalOutput);
|
|
573
|
+
const lines = [
|
|
574
|
+
"",
|
|
575
|
+
"[delegate-task retry suggestion]",
|
|
576
|
+
`Error type: ${errorInfo.errorType}`,
|
|
577
|
+
`Fix: ${pattern.fixHint}`
|
|
578
|
+
];
|
|
579
|
+
if (available) {
|
|
580
|
+
lines.push(`Available: ${available}`);
|
|
517
581
|
}
|
|
518
|
-
|
|
519
|
-
|
|
582
|
+
if (errorInfo.errorType === "background_agent_not_allowed") {
|
|
583
|
+
lines.push(
|
|
584
|
+
"Retry now with corrected parameters.",
|
|
585
|
+
"Use `background=true` only for `explorer` or `librarian`; otherwise retry without background.",
|
|
586
|
+
"If the host enables `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`, collect results with `task_status` before reporting completion.",
|
|
587
|
+
"Examples:",
|
|
588
|
+
'task(description="...", prompt="...", subagent_type="explorer", background=true)',
|
|
589
|
+
'task_status(task_id="ses_...", wait=true)'
|
|
590
|
+
);
|
|
591
|
+
return lines.join("\n");
|
|
592
|
+
}
|
|
593
|
+
lines.push(
|
|
594
|
+
"Retry now with corrected parameters. Example:",
|
|
595
|
+
'task(description="...", prompt="...", subagent_type="explorer")'
|
|
596
|
+
);
|
|
597
|
+
return lines.join("\n");
|
|
520
598
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const stdout = await new Response(proc.stdout).text();
|
|
535
|
-
const path5 = stdout.trim().split("\n")[0];
|
|
536
|
-
if (!path5) {
|
|
537
|
-
log("[tmux] findTmuxPath: no path in output");
|
|
538
|
-
return null;
|
|
539
|
-
}
|
|
540
|
-
const verifyProc = spawn([path5, "-V"], {
|
|
541
|
-
stdout: "pipe",
|
|
542
|
-
stderr: "pipe"
|
|
543
|
-
});
|
|
544
|
-
const verifyExit = await verifyProc.exited;
|
|
545
|
-
if (verifyExit !== 0) {
|
|
546
|
-
log("[tmux] findTmuxPath: tmux -V failed", { path: path5, verifyExit });
|
|
547
|
-
return null;
|
|
599
|
+
|
|
600
|
+
// src/hooks/delegate-task-retry/hook.ts
|
|
601
|
+
function createDelegateTaskRetryHook(_ctx) {
|
|
602
|
+
return {
|
|
603
|
+
"tool.execute.after": async (input, output) => {
|
|
604
|
+
const toolName = input.tool.toLowerCase();
|
|
605
|
+
const isDelegateTool = toolName === "task";
|
|
606
|
+
if (!isDelegateTool) return;
|
|
607
|
+
if (typeof output.output !== "string") return;
|
|
608
|
+
const detected = detectDelegateTaskError(output.output);
|
|
609
|
+
if (!detected) return;
|
|
610
|
+
output.output += `
|
|
611
|
+
${buildRetryGuidance(detected)}`;
|
|
548
612
|
}
|
|
549
|
-
|
|
550
|
-
return path5;
|
|
551
|
-
} catch (err) {
|
|
552
|
-
log("[tmux] findTmuxPath: exception", { error: String(err) });
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
613
|
+
};
|
|
555
614
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
615
|
+
|
|
616
|
+
// src/hooks/foreground-fallback/index.ts
|
|
617
|
+
var RATE_LIMIT_PATTERNS = [
|
|
618
|
+
/\b429\b/,
|
|
619
|
+
/rate.?limit/i,
|
|
620
|
+
/too many requests/i,
|
|
621
|
+
/quota.?exceeded/i,
|
|
622
|
+
/usage.?exceeded/i,
|
|
623
|
+
/usage limit/i,
|
|
624
|
+
/overloaded/i,
|
|
625
|
+
/resource.?exhausted/i,
|
|
626
|
+
/insufficient.?quota/i,
|
|
627
|
+
/high concurrency/i,
|
|
628
|
+
/reduce concurrency/i
|
|
629
|
+
];
|
|
630
|
+
function isRateLimitError(error) {
|
|
631
|
+
if (!error || typeof error !== "object") return false;
|
|
632
|
+
const err = error;
|
|
633
|
+
const text = [
|
|
634
|
+
err.message ?? "",
|
|
635
|
+
String(err.data?.statusCode ?? ""),
|
|
636
|
+
err.data?.message ?? "",
|
|
637
|
+
err.data?.responseBody ?? ""
|
|
638
|
+
].join(" ");
|
|
639
|
+
return RATE_LIMIT_PATTERNS.some((p) => p.test(text));
|
|
564
640
|
}
|
|
565
|
-
function
|
|
566
|
-
|
|
641
|
+
function parseModel(model) {
|
|
642
|
+
const slash = model.indexOf("/");
|
|
643
|
+
if (slash <= 0 || slash >= model.length - 1) return null;
|
|
644
|
+
return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
|
|
567
645
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
await layoutProc.exited;
|
|
575
|
-
if (layout === "main-horizontal" || layout === "main-vertical") {
|
|
576
|
-
const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
|
|
577
|
-
const sizeProc = spawn(
|
|
578
|
-
[tmux, "set-window-option", sizeOption, `${mainPaneSize}%`],
|
|
579
|
-
{
|
|
580
|
-
stdout: "pipe",
|
|
581
|
-
stderr: "pipe"
|
|
582
|
-
}
|
|
583
|
-
);
|
|
584
|
-
await sizeProc.exited;
|
|
585
|
-
const reapplyProc = spawn([tmux, "select-layout", layout], {
|
|
586
|
-
stdout: "pipe",
|
|
587
|
-
stderr: "pipe"
|
|
588
|
-
});
|
|
589
|
-
await reapplyProc.exited;
|
|
590
|
-
}
|
|
591
|
-
log("[tmux] applyLayout: applied", { layout, mainPaneSize });
|
|
592
|
-
} catch (err) {
|
|
593
|
-
log("[tmux] applyLayout: exception", { error: String(err) });
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
async function spawnTmuxPane(sessionId, description, config, serverUrl) {
|
|
597
|
-
log("[tmux] spawnTmuxPane called", {
|
|
598
|
-
sessionId,
|
|
599
|
-
description,
|
|
600
|
-
config,
|
|
601
|
-
serverUrl
|
|
602
|
-
});
|
|
603
|
-
if (!config.enabled) {
|
|
604
|
-
log("[tmux] spawnTmuxPane: config.enabled is false, skipping");
|
|
605
|
-
return { success: false };
|
|
606
|
-
}
|
|
607
|
-
if (!isInsideTmux()) {
|
|
608
|
-
log("[tmux] spawnTmuxPane: not inside tmux, skipping");
|
|
609
|
-
return { success: false };
|
|
610
|
-
}
|
|
611
|
-
const serverRunning = await isServerRunning(serverUrl);
|
|
612
|
-
if (!serverRunning) {
|
|
613
|
-
const defaultPort = process.env.OPENCODE_PORT ?? "4096";
|
|
614
|
-
log("[tmux] spawnTmuxPane: OpenCode server not running, skipping", {
|
|
615
|
-
serverUrl,
|
|
616
|
-
hint: `Start opencode with --port ${defaultPort}`
|
|
617
|
-
});
|
|
618
|
-
return { success: false };
|
|
619
|
-
}
|
|
620
|
-
const tmux = await getTmuxPath();
|
|
621
|
-
if (!tmux) {
|
|
622
|
-
log("[tmux] spawnTmuxPane: tmux binary not found, skipping");
|
|
623
|
-
return { success: false };
|
|
624
|
-
}
|
|
625
|
-
storedConfig = config;
|
|
626
|
-
try {
|
|
627
|
-
const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
|
|
628
|
-
const args = [
|
|
629
|
-
"split-window",
|
|
630
|
-
"-h",
|
|
631
|
-
"-d",
|
|
632
|
-
// Don't switch focus to new pane
|
|
633
|
-
"-P",
|
|
634
|
-
// Print pane info
|
|
635
|
-
"-F",
|
|
636
|
-
"#{pane_id}",
|
|
637
|
-
// Format: just the pane ID
|
|
638
|
-
opencodeCmd
|
|
639
|
-
];
|
|
640
|
-
log("[tmux] spawnTmuxPane: executing", { tmux, args, opencodeCmd });
|
|
641
|
-
const proc = spawn([tmux, ...args], {
|
|
642
|
-
stdout: "pipe",
|
|
643
|
-
stderr: "pipe"
|
|
644
|
-
});
|
|
645
|
-
const exitCode = await proc.exited;
|
|
646
|
-
const stdout = await new Response(proc.stdout).text();
|
|
647
|
-
const stderr = await new Response(proc.stderr).text();
|
|
648
|
-
const paneId = stdout.trim();
|
|
649
|
-
log("[tmux] spawnTmuxPane: split result", {
|
|
650
|
-
exitCode,
|
|
651
|
-
paneId,
|
|
652
|
-
stderr: stderr.trim()
|
|
653
|
-
});
|
|
654
|
-
if (exitCode === 0 && paneId) {
|
|
655
|
-
const renameProc = spawn(
|
|
656
|
-
[tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)],
|
|
657
|
-
{ stdout: "ignore", stderr: "ignore" }
|
|
658
|
-
);
|
|
659
|
-
await renameProc.exited;
|
|
660
|
-
const layout = config.layout ?? "main-vertical";
|
|
661
|
-
const mainPaneSize = config.main_pane_size ?? 60;
|
|
662
|
-
await applyLayout(tmux, layout, mainPaneSize);
|
|
663
|
-
log("[tmux] spawnTmuxPane: SUCCESS, pane created and layout applied", {
|
|
664
|
-
paneId,
|
|
665
|
-
layout
|
|
666
|
-
});
|
|
667
|
-
return { success: true, paneId };
|
|
668
|
-
}
|
|
669
|
-
return { success: false };
|
|
670
|
-
} catch (err) {
|
|
671
|
-
log("[tmux] spawnTmuxPane: exception", { error: String(err) });
|
|
672
|
-
return { success: false };
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
async function closeTmuxPane(paneId) {
|
|
676
|
-
log("[tmux] closeTmuxPane called", { paneId });
|
|
677
|
-
if (!paneId) {
|
|
678
|
-
log("[tmux] closeTmuxPane: no paneId provided");
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
const tmux = await getTmuxPath();
|
|
682
|
-
if (!tmux) {
|
|
683
|
-
log("[tmux] closeTmuxPane: tmux binary not found");
|
|
684
|
-
return false;
|
|
685
|
-
}
|
|
686
|
-
try {
|
|
687
|
-
log("[tmux] closeTmuxPane: sending Ctrl+C for graceful shutdown", {
|
|
688
|
-
paneId
|
|
689
|
-
});
|
|
690
|
-
const ctrlCProc = spawn([tmux, "send-keys", "-t", paneId, "C-c"], {
|
|
691
|
-
stdout: "pipe",
|
|
692
|
-
stderr: "pipe"
|
|
693
|
-
});
|
|
694
|
-
await ctrlCProc.exited;
|
|
695
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
696
|
-
log("[tmux] closeTmuxPane: killing pane", { paneId });
|
|
697
|
-
const proc = spawn([tmux, "kill-pane", "-t", paneId], {
|
|
698
|
-
stdout: "pipe",
|
|
699
|
-
stderr: "pipe"
|
|
700
|
-
});
|
|
701
|
-
const exitCode = await proc.exited;
|
|
702
|
-
const stderr = await new Response(proc.stderr).text();
|
|
703
|
-
log("[tmux] closeTmuxPane: result", { exitCode, stderr: stderr.trim() });
|
|
704
|
-
if (exitCode === 0) {
|
|
705
|
-
log("[tmux] closeTmuxPane: SUCCESS, pane closed", { paneId });
|
|
706
|
-
if (storedConfig) {
|
|
707
|
-
const layout = storedConfig.layout ?? "main-vertical";
|
|
708
|
-
const mainPaneSize = storedConfig.main_pane_size ?? 60;
|
|
709
|
-
await applyLayout(tmux, layout, mainPaneSize);
|
|
710
|
-
log("[tmux] closeTmuxPane: layout reapplied", { layout });
|
|
711
|
-
}
|
|
712
|
-
return true;
|
|
713
|
-
}
|
|
714
|
-
log("[tmux] closeTmuxPane: failed (pane may already be closed)", {
|
|
715
|
-
paneId
|
|
716
|
-
});
|
|
717
|
-
return false;
|
|
718
|
-
} catch (err) {
|
|
719
|
-
log("[tmux] closeTmuxPane: exception", { error: String(err) });
|
|
720
|
-
return false;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
function startTmuxCheck() {
|
|
724
|
-
if (!tmuxChecked) {
|
|
725
|
-
getTmuxPath().catch(() => {
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// src/utils/zip-extractor.ts
|
|
731
|
-
import { release } from "os";
|
|
732
|
-
var WINDOWS_BUILD_WITH_TAR = 17134;
|
|
733
|
-
function getWindowsBuildNumber() {
|
|
734
|
-
if (process.platform !== "win32") return null;
|
|
735
|
-
const parts = release().split(".");
|
|
736
|
-
if (parts.length >= 3) {
|
|
737
|
-
const build = parseInt(parts[2], 10);
|
|
738
|
-
if (!Number.isNaN(build)) return build;
|
|
739
|
-
}
|
|
740
|
-
return null;
|
|
741
|
-
}
|
|
742
|
-
function isPwshAvailable() {
|
|
743
|
-
if (process.platform !== "win32") return false;
|
|
744
|
-
const result = spawnSync(["where", "pwsh"], {
|
|
745
|
-
stdout: "pipe",
|
|
746
|
-
stderr: "pipe"
|
|
747
|
-
});
|
|
748
|
-
return result.exitCode === 0;
|
|
749
|
-
}
|
|
750
|
-
function escapePowerShellPath(path5) {
|
|
751
|
-
return path5.replace(/'/g, "''");
|
|
752
|
-
}
|
|
753
|
-
function getWindowsZipExtractor() {
|
|
754
|
-
const buildNumber = getWindowsBuildNumber();
|
|
755
|
-
if (buildNumber !== null && buildNumber >= WINDOWS_BUILD_WITH_TAR) {
|
|
756
|
-
return "tar";
|
|
757
|
-
}
|
|
758
|
-
if (isPwshAvailable()) {
|
|
759
|
-
return "pwsh";
|
|
760
|
-
}
|
|
761
|
-
return "powershell";
|
|
762
|
-
}
|
|
763
|
-
async function extractZip(archivePath, destDir) {
|
|
764
|
-
let proc;
|
|
765
|
-
if (process.platform === "win32") {
|
|
766
|
-
const extractor = getWindowsZipExtractor();
|
|
767
|
-
switch (extractor) {
|
|
768
|
-
case "tar":
|
|
769
|
-
proc = spawn(["tar", "-xf", archivePath, "-C", destDir], {
|
|
770
|
-
stdout: "ignore",
|
|
771
|
-
stderr: "pipe"
|
|
772
|
-
});
|
|
773
|
-
break;
|
|
774
|
-
case "pwsh":
|
|
775
|
-
proc = spawn(
|
|
776
|
-
[
|
|
777
|
-
"pwsh",
|
|
778
|
-
"-Command",
|
|
779
|
-
`Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
|
|
780
|
-
],
|
|
781
|
-
{
|
|
782
|
-
stdout: "ignore",
|
|
783
|
-
stderr: "pipe"
|
|
784
|
-
}
|
|
785
|
-
);
|
|
786
|
-
break;
|
|
787
|
-
default:
|
|
788
|
-
proc = spawn(
|
|
789
|
-
[
|
|
790
|
-
"powershell",
|
|
791
|
-
"-Command",
|
|
792
|
-
`Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
|
|
793
|
-
],
|
|
794
|
-
{
|
|
795
|
-
stdout: "ignore",
|
|
796
|
-
stderr: "pipe"
|
|
797
|
-
}
|
|
798
|
-
);
|
|
799
|
-
break;
|
|
800
|
-
}
|
|
801
|
-
} else {
|
|
802
|
-
proc = spawn(["unzip", "-o", archivePath, "-d", destDir], {
|
|
803
|
-
stdout: "ignore",
|
|
804
|
-
stderr: "pipe"
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
const exitCode = await proc.exited;
|
|
808
|
-
if (exitCode !== 0) {
|
|
809
|
-
const stderr = await new Response(proc.stderr).text();
|
|
810
|
-
throw new Error(`zip extraction failed (exit ${exitCode}): ${stderr}`);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// src/hooks/chat-headers.ts
|
|
815
|
-
var INTERNAL_MARKER_CACHE_LIMIT = 1e3;
|
|
816
|
-
var internalMarkerCache = /* @__PURE__ */ new Map();
|
|
817
|
-
function getProviderID(input) {
|
|
818
|
-
return input.provider.info?.id || input.model.providerID;
|
|
819
|
-
}
|
|
820
|
-
function isCopilotProvider(providerID) {
|
|
821
|
-
return providerID === "github-copilot" || providerID === "github-copilot-enterprise";
|
|
822
|
-
}
|
|
823
|
-
async function hasInternalMarker(client, sessionID, messageID) {
|
|
824
|
-
const cacheKey = `${sessionID}:${messageID}`;
|
|
825
|
-
const cached = internalMarkerCache.get(cacheKey);
|
|
826
|
-
if (cached !== void 0) {
|
|
827
|
-
return cached;
|
|
828
|
-
}
|
|
829
|
-
try {
|
|
830
|
-
const response = await client.session.message({
|
|
831
|
-
path: { id: sessionID, messageID }
|
|
832
|
-
});
|
|
833
|
-
const hasMarker = (response.data?.parts ?? []).some(
|
|
834
|
-
hasInternalInitiatorMarker
|
|
835
|
-
);
|
|
836
|
-
if (hasMarker) {
|
|
837
|
-
if (internalMarkerCache.size >= INTERNAL_MARKER_CACHE_LIMIT) {
|
|
838
|
-
internalMarkerCache.clear();
|
|
839
|
-
}
|
|
840
|
-
internalMarkerCache.set(cacheKey, true);
|
|
841
|
-
}
|
|
842
|
-
return hasMarker;
|
|
843
|
-
} catch {
|
|
844
|
-
return false;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
function createChatHeadersHook(ctx) {
|
|
848
|
-
return {
|
|
849
|
-
"chat.headers": async (input, output) => {
|
|
850
|
-
if (!isCopilotProvider(getProviderID(input))) {
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
if (input.model.api.npm === "@ai-sdk/github-copilot") {
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
if (!input.message.id || input.message.role !== "user") {
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
if (!await hasInternalMarker(
|
|
860
|
-
ctx.client,
|
|
861
|
-
input.sessionID,
|
|
862
|
-
input.message.id
|
|
863
|
-
)) {
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
output.headers["x-initiator"] = "agent";
|
|
867
|
-
}
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// src/hooks/delegate-task-retry/patterns.ts
|
|
872
|
-
var DELEGATE_TASK_ERROR_PATTERNS = [
|
|
873
|
-
{
|
|
874
|
-
pattern: "load_skills",
|
|
875
|
-
errorType: "missing_load_skills",
|
|
876
|
-
fixHint: "Add load_skills=[] (empty array when no skill is needed)."
|
|
877
|
-
},
|
|
878
|
-
{
|
|
879
|
-
pattern: "category OR subagent_type",
|
|
880
|
-
errorType: "mutual_exclusion",
|
|
881
|
-
fixHint: 'Provide only one: category (e.g., "unspecified-low") OR subagent_type (e.g., "explorer").'
|
|
882
|
-
},
|
|
883
|
-
{
|
|
884
|
-
pattern: "Must provide either category or subagent_type",
|
|
885
|
-
errorType: "missing_category_or_agent",
|
|
886
|
-
fixHint: 'Add either category="unspecified-low" or subagent_type="explorer".'
|
|
887
|
-
},
|
|
888
|
-
{
|
|
889
|
-
pattern: "Unknown category",
|
|
890
|
-
errorType: "unknown_category",
|
|
891
|
-
fixHint: "Use a valid category listed in the error output."
|
|
892
|
-
},
|
|
893
|
-
{
|
|
894
|
-
pattern: "Unknown agent",
|
|
895
|
-
errorType: "unknown_agent",
|
|
896
|
-
fixHint: "Use a valid agent name from the available list."
|
|
897
|
-
},
|
|
898
|
-
{
|
|
899
|
-
pattern: "Skills not found",
|
|
900
|
-
errorType: "unknown_skills",
|
|
901
|
-
fixHint: "Use valid skill names listed in the error output."
|
|
902
|
-
},
|
|
903
|
-
{
|
|
904
|
-
pattern: "is not allowed. Allowed agents:",
|
|
905
|
-
errorType: "background_agent_not_allowed",
|
|
906
|
-
fixHint: "Use one of the allowed agents shown in the error or delegate from a parent agent that can call this subagent."
|
|
907
|
-
}
|
|
908
|
-
];
|
|
909
|
-
function detectDelegateTaskError(output) {
|
|
910
|
-
if (!output || typeof output !== "string") return null;
|
|
911
|
-
const hasErrorSignal = output.includes("[ERROR]") || output.includes("Invalid arguments") || output.includes("is not allowed. Allowed agents:");
|
|
912
|
-
if (!hasErrorSignal) return null;
|
|
913
|
-
for (const pattern of DELEGATE_TASK_ERROR_PATTERNS) {
|
|
914
|
-
if (output.includes(pattern.pattern)) {
|
|
915
|
-
return {
|
|
916
|
-
errorType: pattern.errorType,
|
|
917
|
-
originalOutput: output
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
return null;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// src/hooks/delegate-task-retry/guidance.ts
|
|
925
|
-
function extractAvailableList(output) {
|
|
926
|
-
const match = output.match(/Allowed agents:\s*(.+)$/m);
|
|
927
|
-
if (match) return match[1].trim();
|
|
928
|
-
const available = output.match(/Available[^:]*:\s*(.+)$/m);
|
|
929
|
-
if (available) return available[1].trim();
|
|
930
|
-
return null;
|
|
931
|
-
}
|
|
932
|
-
function buildRetryGuidance(errorInfo) {
|
|
933
|
-
const pattern = DELEGATE_TASK_ERROR_PATTERNS.find(
|
|
934
|
-
(p) => p.errorType === errorInfo.errorType
|
|
935
|
-
);
|
|
936
|
-
if (!pattern) {
|
|
937
|
-
return "\n[delegate-task retry] Fix parameters and retry with corrected arguments.";
|
|
938
|
-
}
|
|
939
|
-
const available = extractAvailableList(errorInfo.originalOutput);
|
|
940
|
-
const lines = [
|
|
941
|
-
"",
|
|
942
|
-
"[delegate-task retry suggestion]",
|
|
943
|
-
`Error type: ${errorInfo.errorType}`,
|
|
944
|
-
`Fix: ${pattern.fixHint}`
|
|
945
|
-
];
|
|
946
|
-
if (available) {
|
|
947
|
-
lines.push(`Available: ${available}`);
|
|
948
|
-
}
|
|
949
|
-
if (errorInfo.errorType === "background_agent_not_allowed") {
|
|
950
|
-
lines.push(
|
|
951
|
-
"Retry now with corrected parameters.",
|
|
952
|
-
"Use `background=true` only for `explorer` or `librarian`; otherwise retry without background.",
|
|
953
|
-
"If the host enables `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`, collect results with `task_status` before reporting completion.",
|
|
954
|
-
"Examples:",
|
|
955
|
-
'task(description="...", prompt="...", subagent_type="explorer", background=true)',
|
|
956
|
-
'task_status(task_id="ses_...", wait=true)'
|
|
957
|
-
);
|
|
958
|
-
return lines.join("\n");
|
|
959
|
-
}
|
|
960
|
-
lines.push(
|
|
961
|
-
"Retry now with corrected parameters. Example:",
|
|
962
|
-
'task(description="...", prompt="...", subagent_type="explorer")'
|
|
963
|
-
);
|
|
964
|
-
return lines.join("\n");
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// src/hooks/delegate-task-retry/hook.ts
|
|
968
|
-
function createDelegateTaskRetryHook(_ctx) {
|
|
969
|
-
return {
|
|
970
|
-
"tool.execute.after": async (input, output) => {
|
|
971
|
-
const toolName = input.tool.toLowerCase();
|
|
972
|
-
const isDelegateTool = toolName === "task";
|
|
973
|
-
if (!isDelegateTool) return;
|
|
974
|
-
if (typeof output.output !== "string") return;
|
|
975
|
-
const detected = detectDelegateTaskError(output.output);
|
|
976
|
-
if (!detected) return;
|
|
977
|
-
output.output += `
|
|
978
|
-
${buildRetryGuidance(detected)}`;
|
|
979
|
-
}
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// src/hooks/foreground-fallback/index.ts
|
|
984
|
-
var RATE_LIMIT_PATTERNS = [
|
|
985
|
-
/\b429\b/,
|
|
986
|
-
/rate.?limit/i,
|
|
987
|
-
/too many requests/i,
|
|
988
|
-
/quota.?exceeded/i,
|
|
989
|
-
/usage.?exceeded/i,
|
|
990
|
-
/usage limit/i,
|
|
991
|
-
/overloaded/i,
|
|
992
|
-
/resource.?exhausted/i,
|
|
993
|
-
/insufficient.?quota/i,
|
|
994
|
-
/high concurrency/i,
|
|
995
|
-
/reduce concurrency/i
|
|
996
|
-
];
|
|
997
|
-
function isRateLimitError(error) {
|
|
998
|
-
if (!error || typeof error !== "object") return false;
|
|
999
|
-
const err = error;
|
|
1000
|
-
const text = [
|
|
1001
|
-
err.message ?? "",
|
|
1002
|
-
String(err.data?.statusCode ?? ""),
|
|
1003
|
-
err.data?.message ?? "",
|
|
1004
|
-
err.data?.responseBody ?? ""
|
|
1005
|
-
].join(" ");
|
|
1006
|
-
return RATE_LIMIT_PATTERNS.some((p) => p.test(text));
|
|
1007
|
-
}
|
|
1008
|
-
function parseModel(model) {
|
|
1009
|
-
const slash = model.indexOf("/");
|
|
1010
|
-
if (slash <= 0 || slash >= model.length - 1) return null;
|
|
1011
|
-
return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
|
|
1012
|
-
}
|
|
1013
|
-
var DEDUP_WINDOW_MS = 5e3;
|
|
1014
|
-
var ForegroundFallbackManager = class {
|
|
1015
|
-
constructor(client, chains, enabled) {
|
|
1016
|
-
this.client = client;
|
|
1017
|
-
this.chains = chains;
|
|
1018
|
-
this.enabled = enabled;
|
|
646
|
+
var DEDUP_WINDOW_MS = 5e3;
|
|
647
|
+
var ForegroundFallbackManager = class {
|
|
648
|
+
constructor(client, chains, enabled) {
|
|
649
|
+
this.client = client;
|
|
650
|
+
this.chains = chains;
|
|
651
|
+
this.enabled = enabled;
|
|
1019
652
|
}
|
|
1020
653
|
client;
|
|
1021
654
|
chains;
|
|
@@ -1259,690 +892,383 @@ function createJsonErrorRecoveryHook(_ctx) {
|
|
|
1259
892
|
if (typeof output.output !== "string") return;
|
|
1260
893
|
if (output.output.includes(JSON_ERROR_REMINDER_MARKER)) return;
|
|
1261
894
|
const outputText = output.output;
|
|
1262
|
-
const hasJsonError = JSON_ERROR_PATTERNS.some(
|
|
1263
|
-
(pattern) => pattern.test(outputText)
|
|
1264
|
-
);
|
|
1265
|
-
if (hasJsonError) {
|
|
1266
|
-
output.output += `
|
|
1267
|
-
${JSON_ERROR_REMINDER}`;
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
// src/hooks/phase-reminder/index.ts
|
|
1274
|
-
var PHASE_REMINDER = `<reminder>Recall Workflow Rules:
|
|
1275
|
-
Understand \u2192 split discovery into surgical probes with explorer/librarian \u2192 synthesize the decision and internal handoff \u2192 execute \u2192 verify.
|
|
1276
|
-
If delegating, write sub-agent prompts in English and launch the specialist in the same turn you mention it. If multiple delegations are independent, emit all tool calls in a single response.
|
|
1277
|
-
Before write-capable dispatch, give concrete scope, anchors, steps, non-goals, and verification.
|
|
1278
|
-
In SDD, after oracle returns [OKAY], give a deep approved-plan overview, then ask the user before implementation.</reminder>`;
|
|
1279
|
-
var PHASE_REMINDER_SEPARATOR = "\n\n---\n\n";
|
|
1280
|
-
function stripPhaseReminder(text) {
|
|
1281
|
-
if (!text) {
|
|
1282
|
-
return "";
|
|
1283
|
-
}
|
|
1284
|
-
const prefix = `${PHASE_REMINDER}${PHASE_REMINDER_SEPARATOR}`;
|
|
1285
|
-
return text.startsWith(prefix) ? text.slice(prefix.length) : text;
|
|
1286
|
-
}
|
|
1287
|
-
function createPhaseReminderHook() {
|
|
1288
|
-
return {
|
|
1289
|
-
"experimental.chat.messages.transform": async (_input, output) => {
|
|
1290
|
-
const { messages } = output;
|
|
1291
|
-
if (messages.length === 0) {
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
let lastUserMessageIndex = -1;
|
|
1295
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1296
|
-
if (messages[i].info.role === "user") {
|
|
1297
|
-
lastUserMessageIndex = i;
|
|
1298
|
-
break;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
if (lastUserMessageIndex === -1) {
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
const lastUserMessage = messages[lastUserMessageIndex];
|
|
1305
|
-
const agent = lastUserMessage.info.agent;
|
|
1306
|
-
if (agent && agent !== "orchestrator") {
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
const textPartIndex = lastUserMessage.parts.findIndex(
|
|
1310
|
-
(p) => p.type === "text" && p.text !== void 0
|
|
895
|
+
const hasJsonError = JSON_ERROR_PATTERNS.some(
|
|
896
|
+
(pattern) => pattern.test(outputText)
|
|
1311
897
|
);
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
const originalText = lastUserMessage.parts[textPartIndex].text ?? "";
|
|
1316
|
-
if (originalText.includes(LITE_INTERNAL_INITIATOR_MARKER)) {
|
|
1317
|
-
return;
|
|
898
|
+
if (hasJsonError) {
|
|
899
|
+
output.output += `
|
|
900
|
+
${JSON_ERROR_REMINDER}`;
|
|
1318
901
|
}
|
|
1319
|
-
lastUserMessage.parts[textPartIndex].text = `${PHASE_REMINDER}${PHASE_REMINDER_SEPARATOR}${originalText}`;
|
|
1320
902
|
}
|
|
1321
903
|
};
|
|
1322
904
|
}
|
|
1323
905
|
|
|
1324
|
-
// src/
|
|
1325
|
-
|
|
1326
|
-
function createPostReadNudgeHook() {
|
|
906
|
+
// src/mcp/index.ts
|
|
907
|
+
function createAllBuiltinMcps() {
|
|
1327
908
|
return {
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1332
|
-
output.output = output.output + NUDGE;
|
|
1333
|
-
}
|
|
909
|
+
exa,
|
|
910
|
+
context7,
|
|
911
|
+
grep_app
|
|
1334
912
|
};
|
|
1335
913
|
}
|
|
914
|
+
function createBuiltinMcps(disabledMcps = []) {
|
|
915
|
+
const allBuiltinMcps = createAllBuiltinMcps();
|
|
916
|
+
return Object.fromEntries(
|
|
917
|
+
Object.entries(allBuiltinMcps).filter(
|
|
918
|
+
([name]) => !disabledMcps.includes(name)
|
|
919
|
+
)
|
|
920
|
+
);
|
|
921
|
+
}
|
|
1336
922
|
|
|
1337
|
-
// src/
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
923
|
+
// src/tools/ast-grep/tools.ts
|
|
924
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
925
|
+
|
|
926
|
+
// src/tools/ast-grep/cli.ts
|
|
927
|
+
import { existsSync as existsSync6 } from "fs";
|
|
928
|
+
|
|
929
|
+
// src/tools/ast-grep/constants.ts
|
|
930
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
931
|
+
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
932
|
+
import { createRequire as createRequire3 } from "module";
|
|
933
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
934
|
+
|
|
935
|
+
// src/tools/ast-grep/downloader.ts
|
|
936
|
+
import { chmodSync, existsSync as existsSync4, mkdirSync, unlinkSync } from "fs";
|
|
937
|
+
import { createRequire as createRequire2 } from "module";
|
|
938
|
+
import { homedir as homedir2 } from "os";
|
|
939
|
+
import { join as join5 } from "path";
|
|
940
|
+
|
|
941
|
+
// src/utils/tmux.ts
|
|
942
|
+
var tmuxPath = null;
|
|
943
|
+
var tmuxChecked = false;
|
|
944
|
+
var storedConfig = null;
|
|
945
|
+
var serverAvailable = null;
|
|
946
|
+
var serverCheckUrl = null;
|
|
947
|
+
async function isServerRunning(serverUrl) {
|
|
948
|
+
if (serverCheckUrl === serverUrl && serverAvailable === true) {
|
|
949
|
+
return true;
|
|
1352
950
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
951
|
+
const healthUrl = new URL("/health", serverUrl).toString();
|
|
952
|
+
const timeoutMs = 3e3;
|
|
953
|
+
const maxAttempts = 2;
|
|
954
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
955
|
+
const controller = new AbortController();
|
|
956
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
957
|
+
let response = null;
|
|
958
|
+
try {
|
|
959
|
+
response = await fetch(healthUrl, { signal: controller.signal }).catch(
|
|
960
|
+
() => null
|
|
961
|
+
);
|
|
962
|
+
} finally {
|
|
963
|
+
clearTimeout(timeout);
|
|
964
|
+
}
|
|
965
|
+
const available = response?.ok ?? false;
|
|
966
|
+
if (available) {
|
|
967
|
+
serverCheckUrl = serverUrl;
|
|
968
|
+
serverAvailable = true;
|
|
969
|
+
log("[tmux] isServerRunning: checked", { serverUrl, available, attempt });
|
|
970
|
+
return true;
|
|
971
|
+
}
|
|
972
|
+
if (attempt < maxAttempts) {
|
|
973
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
974
|
+
}
|
|
1357
975
|
}
|
|
1358
|
-
|
|
976
|
+
log("[tmux] isServerRunning: checked", { serverUrl, available: false });
|
|
977
|
+
return false;
|
|
1359
978
|
}
|
|
1360
|
-
function
|
|
1361
|
-
const
|
|
1362
|
-
const
|
|
1363
|
-
const warn = dependencies.warn ?? console.warn;
|
|
1364
|
-
const errorLogger = dependencies.error ?? console.error;
|
|
979
|
+
async function findTmuxPath() {
|
|
980
|
+
const isWindows = process.platform === "win32";
|
|
981
|
+
const cmd = isWindows ? "where" : "which";
|
|
1365
982
|
try {
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
updatedSkills: report.updatedSkills.map(({ skill }) => skill.name),
|
|
1370
|
-
removedSkills: report.removedSkills,
|
|
1371
|
-
skippedSkills: report.skippedSkills.map((skill) => skill.name),
|
|
1372
|
-
failedSkills: report.failedSkills.map(({ skill }) => skill.name)
|
|
983
|
+
const proc = spawn([cmd, "tmux"], {
|
|
984
|
+
stdout: "pipe",
|
|
985
|
+
stderr: "pipe"
|
|
1373
986
|
});
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
987
|
+
const exitCode = await proc.exited;
|
|
988
|
+
if (exitCode !== 0) {
|
|
989
|
+
log("[tmux] findTmuxPath: 'which tmux' failed", { exitCode });
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
const stdout = await new Response(proc.stdout).text();
|
|
993
|
+
const path4 = stdout.trim().split("\n")[0];
|
|
994
|
+
if (!path4) {
|
|
995
|
+
log("[tmux] findTmuxPath: no path in output");
|
|
996
|
+
return null;
|
|
1378
997
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
998
|
+
const verifyProc = spawn([path4, "-V"], {
|
|
999
|
+
stdout: "pipe",
|
|
1000
|
+
stderr: "pipe"
|
|
1001
|
+
});
|
|
1002
|
+
const verifyExit = await verifyProc.exited;
|
|
1003
|
+
if (verifyExit !== 0) {
|
|
1004
|
+
log("[tmux] findTmuxPath: tmux -V failed", { path: path4, verifyExit });
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
log("[tmux] findTmuxPath: found tmux", { path: path4 });
|
|
1008
|
+
return path4;
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
log("[tmux] findTmuxPath: exception", { error: String(err) });
|
|
1011
|
+
return null;
|
|
1382
1012
|
}
|
|
1383
1013
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1014
|
+
async function getTmuxPath() {
|
|
1015
|
+
if (tmuxChecked) {
|
|
1016
|
+
return tmuxPath;
|
|
1017
|
+
}
|
|
1018
|
+
tmuxPath = await findTmuxPath();
|
|
1019
|
+
tmuxChecked = true;
|
|
1020
|
+
log("[tmux] getTmuxPath: initialized", { tmuxPath });
|
|
1021
|
+
return tmuxPath;
|
|
1390
1022
|
}
|
|
1391
|
-
function
|
|
1392
|
-
return
|
|
1023
|
+
function isInsideTmux() {
|
|
1024
|
+
return !!process.env.TMUX;
|
|
1393
1025
|
}
|
|
1394
|
-
function
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1026
|
+
async function applyLayout(tmux, layout, mainPaneSize) {
|
|
1027
|
+
try {
|
|
1028
|
+
const layoutProc = spawn([tmux, "select-layout", layout], {
|
|
1029
|
+
stdout: "pipe",
|
|
1030
|
+
stderr: "pipe"
|
|
1031
|
+
});
|
|
1032
|
+
await layoutProc.exited;
|
|
1033
|
+
if (layout === "main-horizontal" || layout === "main-vertical") {
|
|
1034
|
+
const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
|
|
1035
|
+
const sizeProc = spawn(
|
|
1036
|
+
[tmux, "set-window-option", sizeOption, `${mainPaneSize}%`],
|
|
1037
|
+
{
|
|
1038
|
+
stdout: "pipe",
|
|
1039
|
+
stderr: "pipe"
|
|
1040
|
+
}
|
|
1041
|
+
);
|
|
1042
|
+
await sizeProc.exited;
|
|
1043
|
+
const reapplyProc = spawn([tmux, "select-layout", layout], {
|
|
1044
|
+
stdout: "pipe",
|
|
1045
|
+
stderr: "pipe"
|
|
1046
|
+
});
|
|
1047
|
+
await reapplyProc.exited;
|
|
1048
|
+
}
|
|
1049
|
+
log("[tmux] applyLayout: applied", { layout, mainPaneSize });
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
log("[tmux] applyLayout: exception", { error: String(err) });
|
|
1398
1052
|
}
|
|
1399
|
-
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
1400
1053
|
}
|
|
1401
|
-
function
|
|
1402
|
-
|
|
1403
|
-
|
|
1054
|
+
async function spawnTmuxPane(sessionId, description, config, serverUrl) {
|
|
1055
|
+
log("[tmux] spawnTmuxPane called", {
|
|
1056
|
+
sessionId,
|
|
1057
|
+
description,
|
|
1058
|
+
config,
|
|
1059
|
+
serverUrl
|
|
1060
|
+
});
|
|
1061
|
+
if (!config.enabled) {
|
|
1062
|
+
log("[tmux] spawnTmuxPane: config.enabled is false, skipping");
|
|
1063
|
+
return { success: false };
|
|
1404
1064
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
if (sessions.length === 0 && observations.length === 0 && prompts.length === 0) {
|
|
1409
|
-
return null;
|
|
1065
|
+
if (!isInsideTmux()) {
|
|
1066
|
+
log("[tmux] spawnTmuxPane: not inside tmux, skipping");
|
|
1067
|
+
return { success: false };
|
|
1410
1068
|
}
|
|
1411
|
-
const
|
|
1412
|
-
if (
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
const summary = previewText(rawSession.summary, 300);
|
|
1420
|
-
if (summary) {
|
|
1421
|
-
lines.push(` Summary: ${summary}`);
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
lines.push("");
|
|
1425
|
-
}
|
|
1426
|
-
if (observations.length > 0) {
|
|
1427
|
-
lines.push("### Recent Observations");
|
|
1428
|
-
for (const rawObservation of observations) {
|
|
1429
|
-
const type = normalizeText(rawObservation.type) ?? "manual";
|
|
1430
|
-
const title = normalizeText(rawObservation.title) ?? "Untitled";
|
|
1431
|
-
const createdAt = normalizeText(rawObservation.created_at) ?? "unknown";
|
|
1432
|
-
lines.push(`- [${type}] ${title} (${createdAt})`);
|
|
1433
|
-
const content = previewText(rawObservation.content, 300);
|
|
1434
|
-
if (content) {
|
|
1435
|
-
lines.push(` ${content}`);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
lines.push("");
|
|
1069
|
+
const serverRunning = await isServerRunning(serverUrl);
|
|
1070
|
+
if (!serverRunning) {
|
|
1071
|
+
const defaultPort = process.env.OPENCODE_PORT ?? "4096";
|
|
1072
|
+
log("[tmux] spawnTmuxPane: OpenCode server not running, skipping", {
|
|
1073
|
+
serverUrl,
|
|
1074
|
+
hint: `Start opencode with --port ${defaultPort}`
|
|
1075
|
+
});
|
|
1076
|
+
return { success: false };
|
|
1439
1077
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
if (!content) {
|
|
1445
|
-
continue;
|
|
1446
|
-
}
|
|
1447
|
-
const createdAt = normalizeText(rawPrompt.created_at) ?? "unknown";
|
|
1448
|
-
lines.push(`- ${content} (${createdAt})`);
|
|
1449
|
-
}
|
|
1450
|
-
lines.push("");
|
|
1078
|
+
const tmux = await getTmuxPath();
|
|
1079
|
+
if (!tmux) {
|
|
1080
|
+
log("[tmux] spawnTmuxPane: tmux binary not found, skipping");
|
|
1081
|
+
return { success: false };
|
|
1451
1082
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1083
|
+
storedConfig = config;
|
|
1084
|
+
try {
|
|
1085
|
+
const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
|
|
1086
|
+
const args = [
|
|
1087
|
+
"split-window",
|
|
1088
|
+
"-h",
|
|
1089
|
+
"-d",
|
|
1090
|
+
// Don't switch focus to new pane
|
|
1091
|
+
"-P",
|
|
1092
|
+
// Print pane info
|
|
1093
|
+
"-F",
|
|
1094
|
+
"#{pane_id}",
|
|
1095
|
+
// Format: just the pane ID
|
|
1096
|
+
opencodeCmd
|
|
1097
|
+
];
|
|
1098
|
+
log("[tmux] spawnTmuxPane: executing", { tmux, args, opencodeCmd });
|
|
1099
|
+
const proc = spawn([tmux, ...args], {
|
|
1100
|
+
stdout: "pipe",
|
|
1101
|
+
stderr: "pipe"
|
|
1102
|
+
});
|
|
1103
|
+
const exitCode = await proc.exited;
|
|
1104
|
+
const stdout = await new Response(proc.stdout).text();
|
|
1105
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1106
|
+
const paneId = stdout.trim();
|
|
1107
|
+
log("[tmux] spawnTmuxPane: split result", {
|
|
1108
|
+
exitCode,
|
|
1109
|
+
paneId,
|
|
1110
|
+
stderr: stderr.trim()
|
|
1111
|
+
});
|
|
1112
|
+
if (exitCode === 0 && paneId) {
|
|
1113
|
+
const renameProc = spawn(
|
|
1114
|
+
[tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)],
|
|
1115
|
+
{ stdout: "ignore", stderr: "ignore" }
|
|
1116
|
+
);
|
|
1117
|
+
await renameProc.exited;
|
|
1118
|
+
const layout = config.layout ?? "main-vertical";
|
|
1119
|
+
const mainPaneSize = config.main_pane_size ?? 60;
|
|
1120
|
+
await applyLayout(tmux, layout, mainPaneSize);
|
|
1121
|
+
log("[tmux] spawnTmuxPane: SUCCESS, pane created and layout applied", {
|
|
1122
|
+
paneId,
|
|
1123
|
+
layout
|
|
1124
|
+
});
|
|
1125
|
+
return { success: true, paneId };
|
|
1126
|
+
}
|
|
1127
|
+
return { success: false };
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
log("[tmux] spawnTmuxPane: exception", { error: String(err) });
|
|
1130
|
+
return { success: false };
|
|
1458
1131
|
}
|
|
1459
|
-
return lines.join("\n");
|
|
1460
1132
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1478
|
-
|
|
1133
|
+
async function closeTmuxPane(paneId) {
|
|
1134
|
+
log("[tmux] closeTmuxPane called", { paneId });
|
|
1135
|
+
if (!paneId) {
|
|
1136
|
+
log("[tmux] closeTmuxPane: no paneId provided");
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
const tmux = await getTmuxPath();
|
|
1140
|
+
if (!tmux) {
|
|
1141
|
+
log("[tmux] closeTmuxPane: tmux binary not found");
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
log("[tmux] closeTmuxPane: sending Ctrl+C for graceful shutdown", {
|
|
1146
|
+
paneId
|
|
1147
|
+
});
|
|
1148
|
+
const ctrlCProc = spawn([tmux, "send-keys", "-t", paneId, "C-c"], {
|
|
1149
|
+
stdout: "pipe",
|
|
1150
|
+
stderr: "pipe"
|
|
1479
1151
|
});
|
|
1480
|
-
|
|
1481
|
-
|
|
1152
|
+
await ctrlCProc.exited;
|
|
1153
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
1154
|
+
log("[tmux] closeTmuxPane: killing pane", { paneId });
|
|
1155
|
+
const proc = spawn([tmux, "kill-pane", "-t", paneId], {
|
|
1156
|
+
stdout: "pipe",
|
|
1157
|
+
stderr: "pipe"
|
|
1158
|
+
});
|
|
1159
|
+
const exitCode = await proc.exited;
|
|
1160
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1161
|
+
log("[tmux] closeTmuxPane: result", { exitCode, stderr: stderr.trim() });
|
|
1162
|
+
if (exitCode === 0) {
|
|
1163
|
+
log("[tmux] closeTmuxPane: SUCCESS, pane closed", { paneId });
|
|
1164
|
+
if (storedConfig) {
|
|
1165
|
+
const layout = storedConfig.layout ?? "main-vertical";
|
|
1166
|
+
const mainPaneSize = storedConfig.main_pane_size ?? 60;
|
|
1167
|
+
await applyLayout(tmux, layout, mainPaneSize);
|
|
1168
|
+
log("[tmux] closeTmuxPane: layout reapplied", { layout });
|
|
1169
|
+
}
|
|
1170
|
+
return true;
|
|
1482
1171
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
}
|
|
1486
|
-
async memSessionStart(sessionId) {
|
|
1487
|
-
const response = await this.httpPost("/sessions", {
|
|
1488
|
-
id: sessionId,
|
|
1489
|
-
project: this.project,
|
|
1490
|
-
directory: this.directory
|
|
1172
|
+
log("[tmux] closeTmuxPane: failed (pane may already be closed)", {
|
|
1173
|
+
paneId
|
|
1491
1174
|
});
|
|
1492
|
-
return
|
|
1175
|
+
return false;
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
log("[tmux] closeTmuxPane: exception", { error: String(err) });
|
|
1178
|
+
return false;
|
|
1493
1179
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
project: this.project
|
|
1180
|
+
}
|
|
1181
|
+
function startTmuxCheck() {
|
|
1182
|
+
if (!tmuxChecked) {
|
|
1183
|
+
getTmuxPath().catch(() => {
|
|
1499
1184
|
});
|
|
1500
|
-
return response !== null;
|
|
1501
|
-
}
|
|
1502
|
-
async httpPost(path5, body) {
|
|
1503
|
-
if (!this.enabled) {
|
|
1504
|
-
return null;
|
|
1505
|
-
}
|
|
1506
|
-
try {
|
|
1507
|
-
const response = await fetch(`${this.baseUrl}${path5}`, {
|
|
1508
|
-
method: "POST",
|
|
1509
|
-
headers: { "Content-Type": "application/json" },
|
|
1510
|
-
body: JSON.stringify(body),
|
|
1511
|
-
signal: AbortSignal.timeout(this.timeoutMs)
|
|
1512
|
-
});
|
|
1513
|
-
if (!response.ok) {
|
|
1514
|
-
return null;
|
|
1515
|
-
}
|
|
1516
|
-
return await response.json();
|
|
1517
|
-
} catch (error) {
|
|
1518
|
-
log("[thoth] HTTP POST unavailable", {
|
|
1519
|
-
path: path5,
|
|
1520
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1521
|
-
});
|
|
1522
|
-
return null;
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
async httpGet(path5) {
|
|
1526
|
-
if (!this.enabled) {
|
|
1527
|
-
return null;
|
|
1528
|
-
}
|
|
1529
|
-
try {
|
|
1530
|
-
const response = await fetch(`${this.baseUrl}${path5}`, {
|
|
1531
|
-
signal: AbortSignal.timeout(this.timeoutMs)
|
|
1532
|
-
});
|
|
1533
|
-
if (!response.ok) {
|
|
1534
|
-
return null;
|
|
1535
|
-
}
|
|
1536
|
-
return await response.json();
|
|
1537
|
-
} catch (error) {
|
|
1538
|
-
log("[thoth] HTTP GET unavailable", {
|
|
1539
|
-
path: path5,
|
|
1540
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1541
|
-
});
|
|
1542
|
-
return null;
|
|
1543
|
-
}
|
|
1544
1185
|
}
|
|
1545
|
-
};
|
|
1546
|
-
function createThothClient(options) {
|
|
1547
|
-
return new HttpThothClient(options);
|
|
1548
1186
|
}
|
|
1549
1187
|
|
|
1550
|
-
// src/
|
|
1551
|
-
|
|
1552
|
-
var
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
[User preferences or constraints discovered during this session]
|
|
1560
|
-
|
|
1561
|
-
## Discoveries
|
|
1562
|
-
- [Technical findings, gotchas, non-obvious learnings]
|
|
1563
|
-
|
|
1564
|
-
## Accomplished
|
|
1565
|
-
- [Completed items with key details]
|
|
1566
|
-
|
|
1567
|
-
## Next Steps
|
|
1568
|
-
- [What remains to be done]
|
|
1569
|
-
|
|
1570
|
-
## Relevant Files
|
|
1571
|
-
- path/to/file.ts - [what it does or what changed]`;
|
|
1572
|
-
function buildCompactionReminder(sessionID) {
|
|
1573
|
-
return `FIRST ACTION REQUIRED: this session was compacted. Call \`mem_session(action="summary")\` with the content of the compacted summary and \`session_id\` \`${sessionID}\`. This preserves what was accomplished before compaction. Do this BEFORE any other work. After that, call \`mem_context(recall_query=...)\` for fused recent context, and use the recall funnel (\`mem_recall(mode="compact")\` -> \`mem_recall(mode="context")\` -> \`mem_get(...)\`) for precise retrieval.`;
|
|
1574
|
-
}
|
|
1575
|
-
function buildCompactorInstruction(project) {
|
|
1576
|
-
return `CRITICAL INSTRUCTION: place this at the TOP of the compacted summary exactly as an action item for the resumed agent: "FIRST ACTION REQUIRED: Call mem_session(action="summary") with the content of this compacted summary. Use project: '${project}'. This preserves what was accomplished before compaction. Do this BEFORE any other work."`;
|
|
1577
|
-
}
|
|
1578
|
-
function buildMemoryInstructions(sessionID, project) {
|
|
1579
|
-
return `
|
|
1580
|
-
<memory_protocol>
|
|
1581
|
-
Persistent memory is available through thoth-mem. Follow this protocol.
|
|
1582
|
-
|
|
1583
|
-
IMPORTANT: Your current session_id is \`${sessionID}\` and project is \`${project}\`.
|
|
1584
|
-
Always pass these values when calling memory tools that accept them.
|
|
1585
|
-
|
|
1586
|
-
### CORE TOOLS
|
|
1587
|
-
mem_save, mem_recall, mem_context, mem_get, mem_project, mem_session
|
|
1588
|
-
|
|
1589
|
-
### OWNERSHIP AND SUBAGENT HANDOFF
|
|
1590
|
-
- Root owns \`mem_session(action="start"|"checkpoint"|"summary")\` and \`mem_save(kind="prompt"|"session_summary")\`.
|
|
1591
|
-
- Start root memory-backed workflows with \`mem_session(action="start")\` before any other thoth-mem operation when tools and identity are available.
|
|
1592
|
-
- Save the real user prompt with \`mem_save(kind="prompt")\`; never save generated subagent prompts as user intent.
|
|
1593
|
-
- Before memory-dependent delegation, save the handoff body with \`mem_session(action="summary")\` or \`mem_save(kind="session_summary")\`.
|
|
1594
|
-
- Subagent handoff prompts carry parent \`session_id\`, \`project\`, permissions, and recovery instructions only; do not ask subagents to own session lifecycle actions or save prompts.
|
|
1595
|
-
|
|
1596
|
-
### WHEN TO SAVE
|
|
1597
|
-
Call \`mem_save(kind="observation")\` IMMEDIATELY after ANY of these:
|
|
1598
|
-
- Architecture, design, or workflow decision made
|
|
1599
|
-
- Bug fixed (include root cause)
|
|
1600
|
-
- Non-obvious discovery, gotcha, or edge case found
|
|
1601
|
-
- Configuration change or environment setup
|
|
1602
|
-
- Pattern or convention established (naming, structure, approach)
|
|
1603
|
-
- User preference or constraint learned
|
|
1604
|
-
- Feature implemented with non-obvious approach
|
|
1605
|
-
- User confirms a recommendation ("dale", "go with that", "sounds good", "s\xED, esa")
|
|
1606
|
-
- User rejects an approach or expresses a preference ("no, better X", "I prefer X")
|
|
1607
|
-
- Discussion concludes with a clear direction chosen
|
|
1608
|
-
|
|
1609
|
-
Use \`title\` as Verb + what changed or was learned.
|
|
1610
|
-
Use \`kind\` intentionally: \`observation\`, \`prompt\`, \`session_summary\`, or \`passive_learnings\`.
|
|
1611
|
-
Use \`type\` from: bugfix | decision | architecture | discovery | pattern | config | learning | manual.
|
|
1612
|
-
Set \`scope\` intentionally.
|
|
1613
|
-
Reuse a stable \`topic_key\` for the same evolving topic. Do not overwrite unrelated topics.
|
|
1614
|
-
Put durable observation details in \`content\` with this structure:
|
|
1615
|
-
- What: concise description of what changed or was learned
|
|
1616
|
-
- Why: why it mattered or what problem it solved
|
|
1617
|
-
- Where: files, paths, or systems involved
|
|
1618
|
-
- Learned: edge cases, caveats, or follow-up notes
|
|
1619
|
-
|
|
1620
|
-
**Self-check after EVERY task**: "Did I or the user just make a decision, confirm a recommendation, express a preference, fix a bug, learn something, or establish a convention? If yes \u2192 mem_save(kind="observation") NOW."
|
|
1621
|
-
|
|
1622
|
-
### RECALL FUNNEL
|
|
1623
|
-
- Broad recovery (session start, after compaction): call \`mem_context(recall_query=...)\` for fused recent context when useful.
|
|
1624
|
-
- Targeted retrieval:
|
|
1625
|
-
1. Call \`mem_recall(mode="compact")\` to scan candidate IDs and titles.
|
|
1626
|
-
2. Call \`mem_recall(mode="context")\` to expand the strongest hits into retrieved context.
|
|
1627
|
-
3. Call \`mem_get(...)\` only for records you need in full.
|
|
1628
|
-
- Use HyDE/fused recall for semantic or ambiguous searches.
|
|
1629
|
-
- Set \`mem_recall\` \`limit\` from 1 to 20 for candidate/result count.
|
|
1630
|
-
- Narrow with \`topic_key\`, \`type\`, \`time_from\`, \`time_to\`, \`scope\`, \`project\`, and \`session_id\` filters.
|
|
1631
|
-
- Use \`mem_get\` with \`kind="observation"|"prompt"\`; use \`mem_get(include_timeline=true)\` with \`before\`/\`after\`, and \`offset\`/\`max_length\` for large content.
|
|
1632
|
-
- Use bounded \`mem_project(action="graph"|"topics"|"topic")\` for relationship and topic navigation; \`mem_project(action="graph")\` relations are \`HAS_TYPE\`, \`IN_PROJECT\`, \`HAS_TOPIC_KEY\`, \`HAS_WHAT\`, \`HAS_WHY\`, \`HAS_WHERE\`, and \`HAS_LEARNED\`; these calls supplement, not replace, the recall funnel.
|
|
1633
|
-
- Search proactively on the first message about a project, feature, or problem when prior context may matter.
|
|
1634
|
-
- Search before starting work that may have been done before.
|
|
1635
|
-
- Search when the user mentions a topic that lacks enough local context.
|
|
1636
|
-
|
|
1637
|
-
### SESSION CLOSE PROTOCOL
|
|
1638
|
-
- Before ending the session, call \`mem_session(action="summary")\` with this exact template.
|
|
1639
|
-
- This is NOT optional. If you skip this, the next session starts blind.
|
|
1640
|
-
- Do not claim memory was saved unless the tool call succeeded.
|
|
1641
|
-
- If your response includes \`## Key Learnings:\`, preserve them with \`mem_save(kind="passive_learnings")\`.
|
|
1642
|
-
|
|
1643
|
-
${SESSION_SUMMARY_TEMPLATE}
|
|
1644
|
-
|
|
1645
|
-
### AFTER COMPACTION
|
|
1646
|
-
- IMMEDIATELY call \`mem_session(action="summary")\` with the compacted summary content.
|
|
1647
|
-
- Then call \`mem_context(recall_query=...)\` for fused recent context.
|
|
1648
|
-
- Use the recall funnel (\`mem_recall(mode="compact")\` -> \`mem_recall(mode="context")\` -> \`mem_get(...)\`) for precise artifact/prior-observation retrieval.
|
|
1649
|
-
- Only then continue working.
|
|
1650
|
-
|
|
1651
|
-
### SDD TOPIC KEY CONVENTION
|
|
1652
|
-
- use ${SDD_TOPIC_KEY_FORMAT}
|
|
1653
|
-
- examples: sdd/add-user-auth/spec, sdd/add-user-auth/design, sdd/add-user-auth/tasks
|
|
1654
|
-
</memory_protocol>
|
|
1655
|
-
`.trim();
|
|
1656
|
-
}
|
|
1657
|
-
function buildSaveNudge() {
|
|
1658
|
-
return "MEMORY REMINDER: It has been a while since your last save. If you have made decisions, discoveries, or completed significant work, call mem_save now.";
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
// src/hooks/thoth-mem/index.ts
|
|
1662
|
-
function isRootSession(session) {
|
|
1663
|
-
return !session.parentID;
|
|
1664
|
-
}
|
|
1665
|
-
function getSessionInfo(event) {
|
|
1666
|
-
const info = event.properties?.info;
|
|
1667
|
-
return info?.id ? info : null;
|
|
1668
|
-
}
|
|
1669
|
-
function isTextPart(part) {
|
|
1670
|
-
return part.type === "text";
|
|
1671
|
-
}
|
|
1672
|
-
function extractPromptText(parts) {
|
|
1673
|
-
const text = parts.filter(isTextPart).map((part) => part.text.trim()).filter((part) => part.length > 0).join("\n\n");
|
|
1674
|
-
return text.length > 0 ? text : null;
|
|
1675
|
-
}
|
|
1676
|
-
function appendInstruction(target, instruction) {
|
|
1677
|
-
if (target.includes(instruction)) {
|
|
1678
|
-
return target;
|
|
1679
|
-
}
|
|
1680
|
-
return `${target}
|
|
1681
|
-
|
|
1682
|
-
${instruction}`;
|
|
1683
|
-
}
|
|
1684
|
-
function stripPrivateTags(str) {
|
|
1685
|
-
if (!str) {
|
|
1686
|
-
return "";
|
|
1687
|
-
}
|
|
1688
|
-
return str.replace(/<private>[\s\S]*?<\/private>/gi, "[REDACTED]").trim();
|
|
1689
|
-
}
|
|
1690
|
-
function truncate(str, max) {
|
|
1691
|
-
if (!str) {
|
|
1692
|
-
return "";
|
|
1188
|
+
// src/utils/zip-extractor.ts
|
|
1189
|
+
import { release } from "os";
|
|
1190
|
+
var WINDOWS_BUILD_WITH_TAR = 17134;
|
|
1191
|
+
function getWindowsBuildNumber() {
|
|
1192
|
+
if (process.platform !== "win32") return null;
|
|
1193
|
+
const parts = release().split(".");
|
|
1194
|
+
if (parts.length >= 3) {
|
|
1195
|
+
const build = parseInt(parts[2], 10);
|
|
1196
|
+
if (!Number.isNaN(build)) return build;
|
|
1693
1197
|
}
|
|
1694
|
-
return
|
|
1695
|
-
}
|
|
1696
|
-
function sanitizePromptText(text) {
|
|
1697
|
-
return truncate(stripPrivateTags(stripPhaseReminder(text)), 2e3);
|
|
1198
|
+
return null;
|
|
1698
1199
|
}
|
|
1699
|
-
function
|
|
1700
|
-
if (
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
return
|
|
1200
|
+
function isPwshAvailable() {
|
|
1201
|
+
if (process.platform !== "win32") return false;
|
|
1202
|
+
const result = spawnSync(["where", "pwsh"], {
|
|
1203
|
+
stdout: "pipe",
|
|
1204
|
+
stderr: "pipe"
|
|
1205
|
+
});
|
|
1206
|
+
return result.exitCode === 0;
|
|
1706
1207
|
}
|
|
1707
|
-
function
|
|
1708
|
-
|
|
1709
|
-
return normalized === "mem_save" || normalized.endsWith(".mem_save") || normalized.endsWith("_mem_save");
|
|
1208
|
+
function escapePowerShellPath(path4) {
|
|
1209
|
+
return path4.replace(/'/g, "''");
|
|
1710
1210
|
}
|
|
1711
|
-
function
|
|
1712
|
-
const
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
directory: options.directory,
|
|
1716
|
-
httpPort: options.thoth?.http_port,
|
|
1717
|
-
timeoutMs: options.thoth?.timeout,
|
|
1718
|
-
enabled
|
|
1719
|
-
});
|
|
1720
|
-
const trackedRootSessions = /* @__PURE__ */ new Set();
|
|
1721
|
-
const needsCompactionFollowUp = /* @__PURE__ */ new Set();
|
|
1722
|
-
const sessionCreatedAt = /* @__PURE__ */ new Map();
|
|
1723
|
-
const lastMemSaveAt = /* @__PURE__ */ new Map();
|
|
1724
|
-
const nudgePending = /* @__PURE__ */ new Set();
|
|
1725
|
-
function isTrackedRootSession(sessionId) {
|
|
1726
|
-
return typeof sessionId === "string" && trackedRootSessions.has(sessionId);
|
|
1727
|
-
}
|
|
1728
|
-
function shouldInjectSaveNudge(sessionId) {
|
|
1729
|
-
if (nudgePending.has(sessionId)) {
|
|
1730
|
-
return false;
|
|
1731
|
-
}
|
|
1732
|
-
if (needsCompactionFollowUp.has(sessionId)) {
|
|
1733
|
-
return false;
|
|
1734
|
-
}
|
|
1735
|
-
const createdAt = sessionCreatedAt.get(sessionId);
|
|
1736
|
-
if (!createdAt || Date.now() - createdAt < 5 * 60 * 1e3) {
|
|
1737
|
-
return false;
|
|
1738
|
-
}
|
|
1739
|
-
const lastSave = lastMemSaveAt.get(sessionId);
|
|
1740
|
-
if (lastSave && Date.now() - lastSave < 15 * 60 * 1e3) {
|
|
1741
|
-
return false;
|
|
1742
|
-
}
|
|
1743
|
-
nudgePending.add(sessionId);
|
|
1744
|
-
return true;
|
|
1745
|
-
}
|
|
1746
|
-
async function handleSessionCreated(session) {
|
|
1747
|
-
if (!enabled || !isRootSession(session)) {
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
const started = await thoth.memSessionStart(session.id);
|
|
1751
|
-
if (started) {
|
|
1752
|
-
trackedRootSessions.add(session.id);
|
|
1753
|
-
sessionCreatedAt.set(session.id, Date.now());
|
|
1754
|
-
} else {
|
|
1755
|
-
log("[thoth] session start unavailable, skipping tracking:", session.id);
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
function handleSessionCompacted(session) {
|
|
1759
|
-
if (!enabled || !isRootSession(session) || !session.id) {
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
needsCompactionFollowUp.add(session.id);
|
|
1211
|
+
function getWindowsZipExtractor() {
|
|
1212
|
+
const buildNumber = getWindowsBuildNumber();
|
|
1213
|
+
if (buildNumber !== null && buildNumber >= WINDOWS_BUILD_WITH_TAR) {
|
|
1214
|
+
return "tar";
|
|
1763
1215
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
needsCompactionFollowUp.delete(session.id);
|
|
1767
|
-
sessionCreatedAt.delete(session.id);
|
|
1768
|
-
lastMemSaveAt.delete(session.id);
|
|
1769
|
-
nudgePending.delete(session.id);
|
|
1216
|
+
if (isPwshAvailable()) {
|
|
1217
|
+
return "pwsh";
|
|
1770
1218
|
}
|
|
1771
|
-
return
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1219
|
+
return "powershell";
|
|
1220
|
+
}
|
|
1221
|
+
async function extractZip(archivePath, destDir) {
|
|
1222
|
+
let proc;
|
|
1223
|
+
if (process.platform === "win32") {
|
|
1224
|
+
const extractor = getWindowsZipExtractor();
|
|
1225
|
+
switch (extractor) {
|
|
1226
|
+
case "tar":
|
|
1227
|
+
proc = spawn(["tar", "-xf", archivePath, "-C", destDir], {
|
|
1228
|
+
stdout: "ignore",
|
|
1229
|
+
stderr: "pipe"
|
|
1230
|
+
});
|
|
1231
|
+
break;
|
|
1232
|
+
case "pwsh":
|
|
1233
|
+
proc = spawn(
|
|
1234
|
+
[
|
|
1235
|
+
"pwsh",
|
|
1236
|
+
"-Command",
|
|
1237
|
+
`Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
|
|
1238
|
+
],
|
|
1239
|
+
{
|
|
1240
|
+
stdout: "ignore",
|
|
1241
|
+
stderr: "pipe"
|
|
1786
1242
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1243
|
+
);
|
|
1244
|
+
break;
|
|
1245
|
+
default:
|
|
1246
|
+
proc = spawn(
|
|
1247
|
+
[
|
|
1248
|
+
"powershell",
|
|
1249
|
+
"-Command",
|
|
1250
|
+
`Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
|
|
1251
|
+
],
|
|
1252
|
+
{
|
|
1253
|
+
stdout: "ignore",
|
|
1254
|
+
stderr: "pipe"
|
|
1794
1255
|
}
|
|
1795
|
-
handleSessionDeleted(session);
|
|
1796
|
-
break;
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
},
|
|
1800
|
-
"chat.message": async (input, output) => {
|
|
1801
|
-
void output.message;
|
|
1802
|
-
if (!enabled) {
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
|
-
if (!isTrackedRootSession(input.sessionID)) {
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
const promptText = extractPromptText(output.parts);
|
|
1809
|
-
if (!promptText) {
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
const sanitizedPromptText = sanitizePromptText(promptText);
|
|
1813
|
-
if (!sanitizedPromptText || sanitizedPromptText.length <= 10) {
|
|
1814
|
-
return;
|
|
1815
|
-
}
|
|
1816
|
-
await thoth.memSavePrompt(input.sessionID, sanitizedPromptText);
|
|
1817
|
-
},
|
|
1818
|
-
"experimental.chat.system.transform": async (input, output) => {
|
|
1819
|
-
void input.model;
|
|
1820
|
-
if (!enabled || !input.sessionID) {
|
|
1821
|
-
return;
|
|
1822
|
-
}
|
|
1823
|
-
if (!isTrackedRootSession(input.sessionID)) {
|
|
1824
|
-
return;
|
|
1825
|
-
}
|
|
1826
|
-
const memoryInstructions = buildMemoryInstructions(
|
|
1827
|
-
input.sessionID,
|
|
1828
|
-
options.project
|
|
1829
|
-
);
|
|
1830
|
-
const compactionReminder = needsCompactionFollowUp.has(input.sessionID) ? buildCompactionReminder(input.sessionID) : null;
|
|
1831
|
-
const saveNudge = shouldInjectSaveNudge(input.sessionID) ? buildSaveNudge() : null;
|
|
1832
|
-
if (output.system.length === 0) {
|
|
1833
|
-
let systemPrompt = memoryInstructions;
|
|
1834
|
-
if (compactionReminder) {
|
|
1835
|
-
systemPrompt = appendInstruction(systemPrompt, compactionReminder);
|
|
1836
|
-
}
|
|
1837
|
-
if (saveNudge) {
|
|
1838
|
-
systemPrompt = appendInstruction(systemPrompt, saveNudge);
|
|
1839
|
-
}
|
|
1840
|
-
output.system.push(systemPrompt);
|
|
1841
|
-
return;
|
|
1842
|
-
}
|
|
1843
|
-
const lastIndex = output.system.length - 1;
|
|
1844
|
-
let updatedSystemPrompt = appendInstruction(
|
|
1845
|
-
output.system[lastIndex],
|
|
1846
|
-
memoryInstructions
|
|
1847
|
-
);
|
|
1848
|
-
if (compactionReminder) {
|
|
1849
|
-
updatedSystemPrompt = appendInstruction(
|
|
1850
|
-
updatedSystemPrompt,
|
|
1851
|
-
compactionReminder
|
|
1852
1256
|
);
|
|
1853
|
-
|
|
1854
|
-
if (saveNudge) {
|
|
1855
|
-
updatedSystemPrompt = appendInstruction(updatedSystemPrompt, saveNudge);
|
|
1856
|
-
}
|
|
1857
|
-
output.system[lastIndex] = updatedSystemPrompt;
|
|
1858
|
-
},
|
|
1859
|
-
"experimental.session.compacting": async (input, output) => {
|
|
1860
|
-
void output.prompt;
|
|
1861
|
-
if (!enabled) {
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
|
-
if (!isTrackedRootSession(input.sessionID)) {
|
|
1865
|
-
return;
|
|
1866
|
-
}
|
|
1867
|
-
needsCompactionFollowUp.add(input.sessionID);
|
|
1868
|
-
output.context.push(buildCompactorInstruction(options.project));
|
|
1869
|
-
output.context.push(FIRST_ACTION_INSTRUCTION);
|
|
1870
|
-
const memoryContext = await thoth.memContext(input.sessionID);
|
|
1871
|
-
if (memoryContext) {
|
|
1872
|
-
output.context.push(memoryContext);
|
|
1873
|
-
}
|
|
1874
|
-
},
|
|
1875
|
-
"tool.execute.after": async (input, output) => {
|
|
1876
|
-
void input.callID;
|
|
1877
|
-
void output.title;
|
|
1878
|
-
void output.output;
|
|
1879
|
-
void output.metadata;
|
|
1880
|
-
if (!enabled) {
|
|
1881
|
-
return;
|
|
1882
|
-
}
|
|
1883
|
-
if (isMemSaveTool(input.tool)) {
|
|
1884
|
-
lastMemSaveAt.set(input.sessionID, Date.now());
|
|
1885
|
-
nudgePending.delete(input.sessionID);
|
|
1886
|
-
}
|
|
1887
|
-
if (isSessionSummaryTool(input.tool, input.args)) {
|
|
1888
|
-
needsCompactionFollowUp.delete(input.sessionID);
|
|
1889
|
-
}
|
|
1257
|
+
break;
|
|
1890
1258
|
}
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
type: "local",
|
|
1903
|
-
command: [...config?.command ?? DEFAULT_THOTH_COMMAND],
|
|
1904
|
-
...Object.keys(environment).length > 0 ? { environment } : {},
|
|
1905
|
-
...typeof config?.timeout === "number" ? { timeout: config.timeout } : {}
|
|
1906
|
-
};
|
|
1907
|
-
return mcpConfig;
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
// src/mcp/index.ts
|
|
1911
|
-
function createAllBuiltinMcps(thothConfig) {
|
|
1912
|
-
return {
|
|
1913
|
-
exa,
|
|
1914
|
-
context7,
|
|
1915
|
-
grep_app,
|
|
1916
|
-
thoth_mem: createThothMcp(thothConfig)
|
|
1917
|
-
};
|
|
1918
|
-
}
|
|
1919
|
-
function createBuiltinMcps(disabledMcps = [], thothConfig) {
|
|
1920
|
-
const allBuiltinMcps = createAllBuiltinMcps(thothConfig);
|
|
1921
|
-
return Object.fromEntries(
|
|
1922
|
-
Object.entries(allBuiltinMcps).filter(
|
|
1923
|
-
([name]) => !disabledMcps.includes(name)
|
|
1924
|
-
)
|
|
1925
|
-
);
|
|
1259
|
+
} else {
|
|
1260
|
+
proc = spawn(["unzip", "-o", archivePath, "-d", destDir], {
|
|
1261
|
+
stdout: "ignore",
|
|
1262
|
+
stderr: "pipe"
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
const exitCode = await proc.exited;
|
|
1266
|
+
if (exitCode !== 0) {
|
|
1267
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1268
|
+
throw new Error(`zip extraction failed (exit ${exitCode}): ${stderr}`);
|
|
1269
|
+
}
|
|
1926
1270
|
}
|
|
1927
1271
|
|
|
1928
|
-
// src/tools/ast-grep/tools.ts
|
|
1929
|
-
import { tool } from "@opencode-ai/plugin/tool";
|
|
1930
|
-
|
|
1931
|
-
// src/tools/ast-grep/cli.ts
|
|
1932
|
-
import { existsSync as existsSync5 } from "fs";
|
|
1933
|
-
|
|
1934
|
-
// src/tools/ast-grep/constants.ts
|
|
1935
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
1936
|
-
import { existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
1937
|
-
import { createRequire as createRequire3 } from "module";
|
|
1938
|
-
import { dirname as dirname2, join as join5 } from "path";
|
|
1939
|
-
|
|
1940
|
-
// src/tools/ast-grep/downloader.ts
|
|
1941
|
-
import { chmodSync, existsSync as existsSync3, mkdirSync, unlinkSync } from "fs";
|
|
1942
|
-
import { createRequire as createRequire2 } from "module";
|
|
1943
|
-
import { homedir as homedir2 } from "os";
|
|
1944
|
-
import { join as join4 } from "path";
|
|
1945
|
-
|
|
1946
1272
|
// src/utils/file-io.ts
|
|
1947
1273
|
import { readFile, writeFile } from "fs/promises";
|
|
1948
1274
|
async function writeTextFile(filePath, contents) {
|
|
@@ -1976,19 +1302,19 @@ var PLATFORM_MAP = {
|
|
|
1976
1302
|
function getCacheDir2() {
|
|
1977
1303
|
if (process.platform === "win32") {
|
|
1978
1304
|
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
1979
|
-
const base2 = localAppData ||
|
|
1980
|
-
return
|
|
1305
|
+
const base2 = localAppData || join5(homedir2(), "AppData", "Local");
|
|
1306
|
+
return join5(base2, "thoth-agents", "bin");
|
|
1981
1307
|
}
|
|
1982
1308
|
const xdgCache = process.env.XDG_CACHE_HOME;
|
|
1983
|
-
const base = xdgCache ||
|
|
1984
|
-
return
|
|
1309
|
+
const base = xdgCache || join5(homedir2(), ".cache");
|
|
1310
|
+
return join5(base, "thoth-agents", "bin");
|
|
1985
1311
|
}
|
|
1986
1312
|
function getBinaryName() {
|
|
1987
1313
|
return process.platform === "win32" ? "sg.exe" : "sg";
|
|
1988
1314
|
}
|
|
1989
1315
|
function getCachedBinaryPath() {
|
|
1990
|
-
const binaryPath =
|
|
1991
|
-
return
|
|
1316
|
+
const binaryPath = join5(getCacheDir2(), getBinaryName());
|
|
1317
|
+
return existsSync4(binaryPath) ? binaryPath : null;
|
|
1992
1318
|
}
|
|
1993
1319
|
async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
1994
1320
|
const platformKey = `${process.platform}-${process.arch}`;
|
|
@@ -2001,8 +1327,8 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
2001
1327
|
}
|
|
2002
1328
|
const cacheDir = getCacheDir2();
|
|
2003
1329
|
const binaryName = getBinaryName();
|
|
2004
|
-
const binaryPath =
|
|
2005
|
-
if (
|
|
1330
|
+
const binaryPath = join5(cacheDir, binaryName);
|
|
1331
|
+
if (existsSync4(binaryPath)) {
|
|
2006
1332
|
return binaryPath;
|
|
2007
1333
|
}
|
|
2008
1334
|
const { arch, os: os2 } = platformInfo;
|
|
@@ -2010,21 +1336,21 @@ async function downloadAstGrep(version = DEFAULT_VERSION) {
|
|
|
2010
1336
|
const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`;
|
|
2011
1337
|
console.log(`[thoth-agents] Downloading ast-grep binary...`);
|
|
2012
1338
|
try {
|
|
2013
|
-
if (!
|
|
1339
|
+
if (!existsSync4(cacheDir)) {
|
|
2014
1340
|
mkdirSync(cacheDir, { recursive: true });
|
|
2015
1341
|
}
|
|
2016
1342
|
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
2017
1343
|
if (!response.ok) {
|
|
2018
1344
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
2019
1345
|
}
|
|
2020
|
-
const archivePath =
|
|
1346
|
+
const archivePath = join5(cacheDir, assetName);
|
|
2021
1347
|
const arrayBuffer = await response.arrayBuffer();
|
|
2022
1348
|
await writeTextFile(archivePath, arrayBuffer);
|
|
2023
1349
|
await extractZip(archivePath, cacheDir);
|
|
2024
|
-
if (
|
|
1350
|
+
if (existsSync4(archivePath)) {
|
|
2025
1351
|
unlinkSync(archivePath);
|
|
2026
1352
|
}
|
|
2027
|
-
if (process.platform !== "win32" &&
|
|
1353
|
+
if (process.platform !== "win32" && existsSync4(binaryPath)) {
|
|
2028
1354
|
chmodSync(binaryPath, 493);
|
|
2029
1355
|
}
|
|
2030
1356
|
console.log(`[thoth-agents] ast-grep binary ready.`);
|
|
@@ -2108,8 +1434,8 @@ function findSgCliPathSync() {
|
|
|
2108
1434
|
const require3 = createRequire3(import.meta.url);
|
|
2109
1435
|
const cliPkgPath = require3.resolve("@ast-grep/cli/package.json");
|
|
2110
1436
|
const cliDir = dirname2(cliPkgPath);
|
|
2111
|
-
const sgPath =
|
|
2112
|
-
if (
|
|
1437
|
+
const sgPath = join6(cliDir, binaryName);
|
|
1438
|
+
if (existsSync5(sgPath) && isValidBinary(sgPath)) {
|
|
2113
1439
|
return sgPath;
|
|
2114
1440
|
}
|
|
2115
1441
|
} catch {
|
|
@@ -2121,8 +1447,8 @@ function findSgCliPathSync() {
|
|
|
2121
1447
|
const pkgPath = require3.resolve(`${platformPkg}/package.json`);
|
|
2122
1448
|
const pkgDir = dirname2(pkgPath);
|
|
2123
1449
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
2124
|
-
const binaryPath =
|
|
2125
|
-
if (
|
|
1450
|
+
const binaryPath = join6(pkgDir, astGrepName);
|
|
1451
|
+
if (existsSync5(binaryPath) && isValidBinary(binaryPath)) {
|
|
2126
1452
|
return binaryPath;
|
|
2127
1453
|
}
|
|
2128
1454
|
} catch {
|
|
@@ -2130,9 +1456,9 @@ function findSgCliPathSync() {
|
|
|
2130
1456
|
}
|
|
2131
1457
|
if (process.platform === "darwin") {
|
|
2132
1458
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
2133
|
-
for (const
|
|
2134
|
-
if (
|
|
2135
|
-
return
|
|
1459
|
+
for (const path4 of homebrewPaths) {
|
|
1460
|
+
if (existsSync5(path4) && isValidBinary(path4)) {
|
|
1461
|
+
return path4;
|
|
2136
1462
|
}
|
|
2137
1463
|
}
|
|
2138
1464
|
}
|
|
@@ -2149,8 +1475,8 @@ function getSgCliPath() {
|
|
|
2149
1475
|
}
|
|
2150
1476
|
return "sg";
|
|
2151
1477
|
}
|
|
2152
|
-
function setSgCliPath(
|
|
2153
|
-
resolvedCliPath =
|
|
1478
|
+
function setSgCliPath(path4) {
|
|
1479
|
+
resolvedCliPath = path4;
|
|
2154
1480
|
}
|
|
2155
1481
|
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
2156
1482
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -2160,7 +1486,7 @@ var DEFAULT_MAX_MATCHES = 500;
|
|
|
2160
1486
|
var initPromise = null;
|
|
2161
1487
|
async function getAstGrepPath() {
|
|
2162
1488
|
const currentPath = getSgCliPath();
|
|
2163
|
-
if (currentPath !== "sg" &&
|
|
1489
|
+
if (currentPath !== "sg" && existsSync6(currentPath)) {
|
|
2164
1490
|
return currentPath;
|
|
2165
1491
|
}
|
|
2166
1492
|
if (initPromise) {
|
|
@@ -2168,7 +1494,7 @@ async function getAstGrepPath() {
|
|
|
2168
1494
|
}
|
|
2169
1495
|
initPromise = (async () => {
|
|
2170
1496
|
const syncPath = findSgCliPathSync();
|
|
2171
|
-
if (syncPath &&
|
|
1497
|
+
if (syncPath && existsSync6(syncPath)) {
|
|
2172
1498
|
setSgCliPath(syncPath);
|
|
2173
1499
|
return syncPath;
|
|
2174
1500
|
}
|
|
@@ -2207,7 +1533,7 @@ async function runSg(options) {
|
|
|
2207
1533
|
const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
|
|
2208
1534
|
args.push(...paths);
|
|
2209
1535
|
let cliPath = getSgCliPath();
|
|
2210
|
-
if (!
|
|
1536
|
+
if (!existsSync6(cliPath) && cliPath !== "sg") {
|
|
2211
1537
|
const downloadedPath = await getAstGrepPath();
|
|
2212
1538
|
if (downloadedPath) {
|
|
2213
1539
|
cliPath = downloadedPath;
|
|
@@ -2500,7 +1826,7 @@ var ast_grep_replace = tool({
|
|
|
2500
1826
|
});
|
|
2501
1827
|
|
|
2502
1828
|
// src/tools/lsp/client.ts
|
|
2503
|
-
import { readFileSync as
|
|
1829
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2504
1830
|
import { extname, resolve as resolve2 } from "path";
|
|
2505
1831
|
import { Readable, Writable } from "stream";
|
|
2506
1832
|
import { pathToFileURL } from "url";
|
|
@@ -2511,9 +1837,9 @@ import {
|
|
|
2511
1837
|
} from "vscode-jsonrpc/node.js";
|
|
2512
1838
|
|
|
2513
1839
|
// src/tools/lsp/config.ts
|
|
2514
|
-
import { existsSync as
|
|
1840
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2515
1841
|
import { homedir as homedir3 } from "os";
|
|
2516
|
-
import { join as
|
|
1842
|
+
import { join as join7 } from "path";
|
|
2517
1843
|
import whichSync from "which";
|
|
2518
1844
|
|
|
2519
1845
|
// src/tools/lsp/config-store.ts
|
|
@@ -2544,7 +1870,7 @@ function hasUserLspConfig() {
|
|
|
2544
1870
|
}
|
|
2545
1871
|
|
|
2546
1872
|
// src/tools/lsp/constants.ts
|
|
2547
|
-
import { existsSync as
|
|
1873
|
+
import { existsSync as existsSync7, readdirSync, statSync as statSync3 } from "fs";
|
|
2548
1874
|
import { dirname as dirname3, resolve } from "path";
|
|
2549
1875
|
var SEVERITY_MAP = {
|
|
2550
1876
|
1: "error",
|
|
@@ -2584,7 +1910,7 @@ function NearestRoot(includePatterns, excludePatterns) {
|
|
|
2584
1910
|
if (excludePatterns) {
|
|
2585
1911
|
for (const dir of walkUpDirectories(file, cwd)) {
|
|
2586
1912
|
for (const pattern of excludePatterns) {
|
|
2587
|
-
if (
|
|
1913
|
+
if (existsSync7(`${dir}/${pattern}`)) {
|
|
2588
1914
|
return void 0;
|
|
2589
1915
|
}
|
|
2590
1916
|
}
|
|
@@ -2603,7 +1929,7 @@ function NearestRoot(includePatterns, excludePatterns) {
|
|
|
2603
1929
|
}
|
|
2604
1930
|
} catch {
|
|
2605
1931
|
}
|
|
2606
|
-
} else if (
|
|
1932
|
+
} else if (existsSync7(`${dir}/${pattern}`)) {
|
|
2607
1933
|
return dir;
|
|
2608
1934
|
}
|
|
2609
1935
|
}
|
|
@@ -3225,11 +2551,11 @@ function isServerInstalled(command) {
|
|
|
3225
2551
|
if (command.length === 0) return false;
|
|
3226
2552
|
const cmd = command[0];
|
|
3227
2553
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
3228
|
-
return
|
|
2554
|
+
return existsSync8(cmd);
|
|
3229
2555
|
}
|
|
3230
2556
|
const isWindows = process.platform === "win32";
|
|
3231
2557
|
const ext = isWindows ? ".exe" : "";
|
|
3232
|
-
const opencodeBin =
|
|
2558
|
+
const opencodeBin = join7(homedir3(), ".config", "opencode", "bin");
|
|
3233
2559
|
const searchPath = (process.env.PATH ?? "") + (isWindows ? ";" : ":") + opencodeBin;
|
|
3234
2560
|
const result = whichSync.sync(cmd, {
|
|
3235
2561
|
path: searchPath,
|
|
@@ -3240,8 +2566,8 @@ function isServerInstalled(command) {
|
|
|
3240
2566
|
return true;
|
|
3241
2567
|
}
|
|
3242
2568
|
const cwd = process.cwd();
|
|
3243
|
-
const localBin =
|
|
3244
|
-
if (
|
|
2569
|
+
const localBin = join7(cwd, "node_modules", ".bin", cmd);
|
|
2570
|
+
if (existsSync8(localBin) || existsSync8(localBin + ext)) {
|
|
3245
2571
|
return true;
|
|
3246
2572
|
}
|
|
3247
2573
|
return false;
|
|
@@ -3581,7 +2907,7 @@ stderr: ${stderr}` : "")
|
|
|
3581
2907
|
log("[lsp] openFile: already open, skipping", { filePath: absPath });
|
|
3582
2908
|
return;
|
|
3583
2909
|
}
|
|
3584
|
-
const text =
|
|
2910
|
+
const text = readFileSync4(absPath, "utf-8");
|
|
3585
2911
|
const ext = extname(absPath);
|
|
3586
2912
|
const languageId = getLanguageId(ext);
|
|
3587
2913
|
log("[lsp] openFile: opening document", {
|
|
@@ -3672,13 +2998,13 @@ import { tool as tool2 } from "@opencode-ai/plugin/tool";
|
|
|
3672
2998
|
|
|
3673
2999
|
// src/tools/lsp/utils.ts
|
|
3674
3000
|
import {
|
|
3675
|
-
existsSync as
|
|
3676
|
-
readFileSync as
|
|
3001
|
+
existsSync as existsSync9,
|
|
3002
|
+
readFileSync as readFileSync5,
|
|
3677
3003
|
statSync as statSync4,
|
|
3678
3004
|
unlinkSync as unlinkSync2,
|
|
3679
3005
|
writeFileSync as writeFileSync3
|
|
3680
3006
|
} from "fs";
|
|
3681
|
-
import { dirname as dirname4, extname as extname2, join as
|
|
3007
|
+
import { dirname as dirname4, extname as extname2, join as join8, resolve as resolve3 } from "path";
|
|
3682
3008
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3683
3009
|
function findServerProjectRoot(filePath, server) {
|
|
3684
3010
|
if (server.root) {
|
|
@@ -3784,7 +3110,7 @@ function filterDiagnosticsBySeverity(diagnostics, severityFilter) {
|
|
|
3784
3110
|
}
|
|
3785
3111
|
function applyTextEditsToFile(filePath, edits) {
|
|
3786
3112
|
try {
|
|
3787
|
-
const content =
|
|
3113
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
3788
3114
|
const lines = content.split("\n");
|
|
3789
3115
|
const sortedEdits = [...edits].sort((a, b) => {
|
|
3790
3116
|
if (b.range.start.line !== a.range.start.line) {
|
|
@@ -3868,7 +3194,7 @@ function applyWorkspaceEdit(edit) {
|
|
|
3868
3194
|
try {
|
|
3869
3195
|
const oldPath = uriToPath(change.oldUri);
|
|
3870
3196
|
const newPath = uriToPath(change.newUri);
|
|
3871
|
-
const content =
|
|
3197
|
+
const content = readFileSync5(oldPath, "utf-8");
|
|
3872
3198
|
writeFileSync3(newPath, content, "utf-8");
|
|
3873
3199
|
unlinkSync2(oldPath);
|
|
3874
3200
|
result.filesModified.push(newPath);
|
|
@@ -4250,17 +3576,16 @@ var TmuxSessionManager = class {
|
|
|
4250
3576
|
};
|
|
4251
3577
|
|
|
4252
3578
|
// src/index.ts
|
|
4253
|
-
function resolveProjectName(project, directory) {
|
|
4254
|
-
const runtimeProjectName = "name" in project && typeof project.name === "string" ? project.name : void 0;
|
|
4255
|
-
return runtimeProjectName || path4.basename(directory) || "thoth-agents";
|
|
4256
|
-
}
|
|
4257
3579
|
var ThothAgents = async (ctx, _options) => {
|
|
4258
|
-
const { client, directory,
|
|
4259
|
-
const projectName = resolveProjectName(project, directory);
|
|
4260
|
-
const sessionContext = /* @__PURE__ */ new Map();
|
|
3580
|
+
const { client, directory, $: shell } = ctx;
|
|
4261
3581
|
const config = loadPluginConfig(directory);
|
|
4262
3582
|
const agentDefs = createAgents(config);
|
|
4263
3583
|
const agents = renderOpenCodeAgentConfigs(config);
|
|
3584
|
+
const packageRoot = fileURLToPath3(new URL("..", import.meta.url));
|
|
3585
|
+
const thothInitCommand = createOpenCodeInitCommand({
|
|
3586
|
+
projectRoot: directory,
|
|
3587
|
+
packageRoot
|
|
3588
|
+
});
|
|
4264
3589
|
const modelArrayMap = {};
|
|
4265
3590
|
for (const agentDef of agentDefs) {
|
|
4266
3591
|
if (agentDef._modelArray && agentDef._modelArray.length > 0) {
|
|
@@ -4298,16 +3623,10 @@ var ThothAgents = async (ctx, _options) => {
|
|
|
4298
3623
|
rawTmuxConfig: config.tmux,
|
|
4299
3624
|
directory
|
|
4300
3625
|
});
|
|
4301
|
-
try {
|
|
4302
|
-
syncSkillsOnStartup();
|
|
4303
|
-
} catch (error) {
|
|
4304
|
-
console.error("[plugin] Failed to sync bundled skills on startup");
|
|
4305
|
-
log("[plugin] Skill sync failed during initialization", error);
|
|
4306
|
-
}
|
|
4307
3626
|
if (tmuxConfig.enabled) {
|
|
4308
3627
|
startTmuxCheck();
|
|
4309
3628
|
}
|
|
4310
|
-
const mcps = createBuiltinMcps(config.disabled_mcps
|
|
3629
|
+
const mcps = createBuiltinMcps(config.disabled_mcps);
|
|
4311
3630
|
const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
|
|
4312
3631
|
const autoUpdateChecker = createAutoUpdateCheckerHook(
|
|
4313
3632
|
ctx,
|
|
@@ -4317,15 +3636,6 @@ var ThothAgents = async (ctx, _options) => {
|
|
|
4317
3636
|
},
|
|
4318
3637
|
shell
|
|
4319
3638
|
);
|
|
4320
|
-
const phaseReminderHook = createPhaseReminderHook();
|
|
4321
|
-
const postReadNudgeHook = createPostReadNudgeHook();
|
|
4322
|
-
const thothMemHook = createThothMemHook({
|
|
4323
|
-
project: projectName,
|
|
4324
|
-
directory,
|
|
4325
|
-
thoth: config.thoth,
|
|
4326
|
-
enabled: true
|
|
4327
|
-
});
|
|
4328
|
-
const chatHeadersHook = createChatHeadersHook(ctx);
|
|
4329
3639
|
const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
|
|
4330
3640
|
const jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
|
|
4331
3641
|
const foregroundFallback = new ForegroundFallbackManager(
|
|
@@ -4348,6 +3658,11 @@ var ThothAgents = async (ctx, _options) => {
|
|
|
4348
3658
|
config: async (opencodeConfig) => {
|
|
4349
3659
|
const lspConfig = opencodeConfig.lsp;
|
|
4350
3660
|
setUserLspConfig(lspConfig);
|
|
3661
|
+
const commands = opencodeConfig.command ?? {};
|
|
3662
|
+
if (!commands["thoth-init"]) {
|
|
3663
|
+
commands["thoth-init"] = thothInitCommand;
|
|
3664
|
+
}
|
|
3665
|
+
opencodeConfig.command = commands;
|
|
4351
3666
|
if (config.setDefaultAgent !== false && !opencodeConfig.default_agent) {
|
|
4352
3667
|
opencodeConfig.default_agent = "orchestrator";
|
|
4353
3668
|
}
|
|
@@ -4416,16 +3731,8 @@ var ThothAgents = async (ctx, _options) => {
|
|
|
4416
3731
|
}
|
|
4417
3732
|
},
|
|
4418
3733
|
event: async (input) => {
|
|
4419
|
-
if (input.event.type === "session.deleted") {
|
|
4420
|
-
const deletedEvent = input.event;
|
|
4421
|
-
const deletedSessionId = deletedEvent.properties?.info?.id ?? deletedEvent.properties?.sessionID;
|
|
4422
|
-
if (deletedSessionId) {
|
|
4423
|
-
sessionContext.delete(deletedSessionId);
|
|
4424
|
-
}
|
|
4425
|
-
}
|
|
4426
3734
|
await foregroundFallback.handleEvent(input.event);
|
|
4427
3735
|
await autoUpdateChecker.event(input);
|
|
4428
|
-
await thothMemHook.event(input);
|
|
4429
3736
|
await tmuxSessionManager.onSessionCreated(
|
|
4430
3737
|
input.event
|
|
4431
3738
|
);
|
|
@@ -4436,56 +3743,7 @@ var ThothAgents = async (ctx, _options) => {
|
|
|
4436
3743
|
input.event
|
|
4437
3744
|
);
|
|
4438
3745
|
},
|
|
4439
|
-
|
|
4440
|
-
"chat.message": async (input, output) => {
|
|
4441
|
-
const chatInput = input;
|
|
4442
|
-
if (chatInput.sessionID) {
|
|
4443
|
-
const previous = sessionContext.get(chatInput.sessionID);
|
|
4444
|
-
const context = {
|
|
4445
|
-
model: previous?.model,
|
|
4446
|
-
variant: previous?.variant
|
|
4447
|
-
};
|
|
4448
|
-
if (typeof chatInput.model?.providerID === "string" && typeof chatInput.model?.modelID === "string") {
|
|
4449
|
-
context.model = {
|
|
4450
|
-
providerID: chatInput.model.providerID,
|
|
4451
|
-
modelID: chatInput.model.modelID
|
|
4452
|
-
};
|
|
4453
|
-
}
|
|
4454
|
-
if (typeof chatInput.variant === "string") {
|
|
4455
|
-
context.variant = chatInput.variant;
|
|
4456
|
-
}
|
|
4457
|
-
sessionContext.set(chatInput.sessionID, context);
|
|
4458
|
-
}
|
|
4459
|
-
if (thothMemHook["chat.message"]) {
|
|
4460
|
-
await thothMemHook["chat.message"](
|
|
4461
|
-
input,
|
|
4462
|
-
output
|
|
4463
|
-
);
|
|
4464
|
-
}
|
|
4465
|
-
},
|
|
4466
|
-
"experimental.chat.system.transform": async (input, output) => {
|
|
4467
|
-
if (thothMemHook["experimental.chat.system.transform"]) {
|
|
4468
|
-
await thothMemHook["experimental.chat.system.transform"](
|
|
4469
|
-
input,
|
|
4470
|
-
output
|
|
4471
|
-
);
|
|
4472
|
-
}
|
|
4473
|
-
},
|
|
4474
|
-
"experimental.chat.messages.transform": async (input, output) => {
|
|
4475
|
-
await phaseReminderHook["experimental.chat.messages.transform"](
|
|
4476
|
-
input,
|
|
4477
|
-
output
|
|
4478
|
-
);
|
|
4479
|
-
},
|
|
4480
|
-
"experimental.session.compacting": async (input, output) => {
|
|
4481
|
-
if (thothMemHook["experimental.session.compacting"]) {
|
|
4482
|
-
await thothMemHook["experimental.session.compacting"](
|
|
4483
|
-
input,
|
|
4484
|
-
output
|
|
4485
|
-
);
|
|
4486
|
-
}
|
|
4487
|
-
},
|
|
4488
|
-
// Post-tool hooks: retry guidance for delegation errors + post-read nudge
|
|
3746
|
+
// Post-tool hooks: retry guidance for delegation and JSON errors.
|
|
4489
3747
|
"tool.execute.after": async (input, output) => {
|
|
4490
3748
|
await delegateTaskRetryHook["tool.execute.after"](
|
|
4491
3749
|
input,
|
|
@@ -4495,16 +3753,6 @@ var ThothAgents = async (ctx, _options) => {
|
|
|
4495
3753
|
input,
|
|
4496
3754
|
output
|
|
4497
3755
|
);
|
|
4498
|
-
if (thothMemHook["tool.execute.after"]) {
|
|
4499
|
-
await thothMemHook["tool.execute.after"](
|
|
4500
|
-
input,
|
|
4501
|
-
output
|
|
4502
|
-
);
|
|
4503
|
-
}
|
|
4504
|
-
await postReadNudgeHook["tool.execute.after"](
|
|
4505
|
-
input,
|
|
4506
|
-
output
|
|
4507
|
-
);
|
|
4508
3756
|
}
|
|
4509
3757
|
};
|
|
4510
3758
|
};
|