micode 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -42
- package/dist/agents/artifact-searcher.d.ts +2 -0
- package/dist/agents/index.d.ts +3 -1
- package/dist/agents/ledger-creator.d.ts +2 -0
- package/dist/hooks/artifact-auto-index.d.ts +9 -0
- package/dist/hooks/auto-clear-ledger.d.ts +11 -0
- package/dist/hooks/comment-checker.d.ts +1 -1
- package/dist/hooks/ledger-loader.d.ts +16 -0
- package/dist/index.js +870 -199
- package/dist/tools/artifact-index/index.d.ts +47 -0
- package/dist/tools/artifact-search.d.ts +18 -0
- package/package.json +9 -3
package/dist/index.js
CHANGED
|
@@ -23,10 +23,11 @@ This is DESIGN ONLY. The planner agent handles detailed implementation plans.
|
|
|
23
23
|
</purpose>
|
|
24
24
|
|
|
25
25
|
<critical-rules>
|
|
26
|
+
<rule>ASK TOOL: Use the ask tool for EVERY question to the user. Never ask in plain text.</rule>
|
|
26
27
|
<rule>NO CODE: Never write code. Never provide code examples. Design only.</rule>
|
|
27
28
|
<rule>SUBAGENTS: Spawn multiple in parallel for codebase analysis.</rule>
|
|
28
29
|
<rule>TOOLS (grep, read, etc.): Do NOT use directly - use subagents instead.</rule>
|
|
29
|
-
<rule>
|
|
30
|
+
<rule>ONE QUESTION AT A TIME: Ask one question, wait for response before continuing.</rule>
|
|
30
31
|
</critical-rules>
|
|
31
32
|
|
|
32
33
|
<available-subagents>
|
|
@@ -53,8 +54,8 @@ This is DESIGN ONLY. The planner agent handles detailed implementation plans.
|
|
|
53
54
|
- codebase-analyzer: "Analyze existing [related feature]"
|
|
54
55
|
- pattern-finder: "Find patterns for [similar functionality]"
|
|
55
56
|
</spawn-example>
|
|
56
|
-
<rule>
|
|
57
|
-
<rule>
|
|
57
|
+
<rule>Use the ask tool for EVERY question - never plain text</rule>
|
|
58
|
+
<rule>Always provide MULTIPLE CHOICE options in the ask tool</rule>
|
|
58
59
|
<focus>purpose, constraints, success criteria</focus>
|
|
59
60
|
</phase>
|
|
60
61
|
|
|
@@ -90,8 +91,9 @@ This is DESIGN ONLY. The planner agent handles detailed implementation plans.
|
|
|
90
91
|
<principle name="design-only">NO CODE. Describe components, not implementations. Planner writes code.</principle>
|
|
91
92
|
<principle name="subagents-first">ALWAYS use subagents for code analysis, NEVER tools directly</principle>
|
|
92
93
|
<principle name="parallel-spawn">Spawn multiple subagents in a SINGLE message</principle>
|
|
93
|
-
<principle name="
|
|
94
|
-
<principle name="
|
|
94
|
+
<principle name="ask-tool-always">ALWAYS use the ask tool for questions - never plain text</principle>
|
|
95
|
+
<principle name="one-question">Ask ONE question at a time via ask tool. Wait for answer.</principle>
|
|
96
|
+
<principle name="multiple-choice">Present 3-5 options in ask tool questions</principle>
|
|
95
97
|
<principle name="yagni">Remove unnecessary features from ALL designs</principle>
|
|
96
98
|
<principle name="explore-alternatives">ALWAYS propose 2-3 approaches before settling</principle>
|
|
97
99
|
<principle name="incremental-validation">Present in sections, validate each before proceeding</principle>
|
|
@@ -867,6 +869,8 @@ Create handoff document to transfer context to future session.
|
|
|
867
869
|
</when-to-use>
|
|
868
870
|
|
|
869
871
|
<rules>
|
|
872
|
+
<rule>FIRST check for existing ledger at thoughts/ledgers/CONTINUITY_*.md</rule>
|
|
873
|
+
<rule>If ledger exists, use its session name for handoff directory</rule>
|
|
870
874
|
<rule>Capture ALL in-progress work</rule>
|
|
871
875
|
<rule>Include exact file:line references for changes</rule>
|
|
872
876
|
<rule>Document learnings and gotchas</rule>
|
|
@@ -876,6 +880,8 @@ Create handoff document to transfer context to future session.
|
|
|
876
880
|
</rules>
|
|
877
881
|
|
|
878
882
|
<process>
|
|
883
|
+
<step>Check for ledger at thoughts/ledgers/CONTINUITY_*.md</step>
|
|
884
|
+
<step>If ledger exists, extract session name and state</step>
|
|
879
885
|
<step>Review what was worked on</step>
|
|
880
886
|
<step>Check git status for uncommitted changes</step>
|
|
881
887
|
<step>Gather learnings and decisions made</step>
|
|
@@ -884,13 +890,17 @@ Create handoff document to transfer context to future session.
|
|
|
884
890
|
<step>Commit handoff document</step>
|
|
885
891
|
</process>
|
|
886
892
|
|
|
887
|
-
<output-path>
|
|
893
|
+
<output-path>
|
|
894
|
+
If ledger exists: thoughts/shared/handoffs/{session-name}/YYYY-MM-DD_HH-MM-SS.md
|
|
895
|
+
Otherwise: thoughts/shared/handoffs/YYYY-MM-DD_HH-MM-SS_description.md
|
|
896
|
+
</output-path>
|
|
888
897
|
|
|
889
898
|
<document-format>
|
|
890
899
|
<frontmatter>
|
|
891
900
|
date: [ISO datetime]
|
|
892
901
|
branch: [branch name]
|
|
893
902
|
commit: [hash]
|
|
903
|
+
session: [session name from ledger, if available]
|
|
894
904
|
</frontmatter>
|
|
895
905
|
<sections>
|
|
896
906
|
<section name="Tasks">Table with Task | Status (completed/in-progress/blocked)</section>
|
|
@@ -1001,6 +1011,10 @@ If you want exception to ANY rule, STOP and get explicit permission first.
|
|
|
1001
1011
|
Breaking the letter or spirit of the rules is failure.
|
|
1002
1012
|
</rule>
|
|
1003
1013
|
|
|
1014
|
+
<rule priority="critical">
|
|
1015
|
+
ALWAYS use the ask tool when you need user input. Never ask questions in plain text.
|
|
1016
|
+
</rule>
|
|
1017
|
+
|
|
1004
1018
|
<values>
|
|
1005
1019
|
<value>Honesty. If you lie, you'll be replaced.</value>
|
|
1006
1020
|
<value>Do it right, not fast. Never skip steps or take shortcuts.</value>
|
|
@@ -1016,7 +1030,8 @@ Breaking the letter or spirit of the rules is failure.
|
|
|
1016
1030
|
<rule>If uncomfortable pushing back, say "Strange things are afoot at the Circle K"</rule>
|
|
1017
1031
|
<rule>STOP and ask for clarification rather than making assumptions</rule>
|
|
1018
1032
|
<rule>STOP and ask for help when human input would be valuable</rule>
|
|
1019
|
-
<rule>
|
|
1033
|
+
<rule>ALWAYS use the ask tool when asking questions - never plain text</rule>
|
|
1034
|
+
<rule>Provide multiple-choice options in the ask tool whenever possible</rule>
|
|
1020
1035
|
</relationship>
|
|
1021
1036
|
|
|
1022
1037
|
<proactiveness>
|
|
@@ -1309,6 +1324,116 @@ var projectInitializerAgent = {
|
|
|
1309
1324
|
prompt: PROMPT2
|
|
1310
1325
|
};
|
|
1311
1326
|
|
|
1327
|
+
// src/agents/ledger-creator.ts
|
|
1328
|
+
var ledgerCreatorAgent = {
|
|
1329
|
+
description: "Creates and updates continuity ledgers for session state preservation",
|
|
1330
|
+
mode: "subagent",
|
|
1331
|
+
model: "anthropic/claude-sonnet-4-20250514",
|
|
1332
|
+
temperature: 0.2,
|
|
1333
|
+
tools: {
|
|
1334
|
+
edit: false,
|
|
1335
|
+
task: false
|
|
1336
|
+
},
|
|
1337
|
+
prompt: `<purpose>
|
|
1338
|
+
Create or update a continuity ledger to preserve session state across context clears.
|
|
1339
|
+
The ledger captures the essential context needed to resume work seamlessly.
|
|
1340
|
+
</purpose>
|
|
1341
|
+
|
|
1342
|
+
<rules>
|
|
1343
|
+
<rule>Keep the ledger CONCISE - only essential information</rule>
|
|
1344
|
+
<rule>Focus on WHAT and WHY, not HOW</rule>
|
|
1345
|
+
<rule>State should have exactly ONE item in "Now"</rule>
|
|
1346
|
+
<rule>Mark uncertain information as UNCONFIRMED</rule>
|
|
1347
|
+
<rule>Include git branch and key file paths</rule>
|
|
1348
|
+
</rules>
|
|
1349
|
+
|
|
1350
|
+
<process>
|
|
1351
|
+
<step>Check for existing ledger at thoughts/ledgers/CONTINUITY_*.md</step>
|
|
1352
|
+
<step>If exists, read and update it</step>
|
|
1353
|
+
<step>If not, create new ledger with session name from current task</step>
|
|
1354
|
+
<step>Gather current state: goal, decisions, progress, blockers</step>
|
|
1355
|
+
<step>Write ledger in the exact format below</step>
|
|
1356
|
+
</process>
|
|
1357
|
+
|
|
1358
|
+
<output-path>thoughts/ledgers/CONTINUITY_{session-name}.md</output-path>
|
|
1359
|
+
|
|
1360
|
+
<ledger-format>
|
|
1361
|
+
# Session: {session-name}
|
|
1362
|
+
Updated: {ISO timestamp}
|
|
1363
|
+
|
|
1364
|
+
## Goal
|
|
1365
|
+
{One sentence describing success criteria}
|
|
1366
|
+
|
|
1367
|
+
## Constraints
|
|
1368
|
+
{Technical requirements, patterns to follow, things to avoid}
|
|
1369
|
+
|
|
1370
|
+
## Key Decisions
|
|
1371
|
+
- {Decision}: {Rationale}
|
|
1372
|
+
|
|
1373
|
+
## State
|
|
1374
|
+
- Done: {Completed items as comma-separated list}
|
|
1375
|
+
- Now: {Current focus - exactly ONE thing}
|
|
1376
|
+
- Next: {Queued items in priority order}
|
|
1377
|
+
|
|
1378
|
+
## Open Questions
|
|
1379
|
+
- UNCONFIRMED: {Things needing verification}
|
|
1380
|
+
|
|
1381
|
+
## Working Set
|
|
1382
|
+
- Branch: \`{branch-name}\`
|
|
1383
|
+
- Key files: \`{paths}\`
|
|
1384
|
+
</ledger-format>
|
|
1385
|
+
|
|
1386
|
+
<output-summary>
|
|
1387
|
+
Ledger updated: thoughts/ledgers/CONTINUITY_{session-name}.md
|
|
1388
|
+
State: {Now item}
|
|
1389
|
+
</output-summary>`
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
// src/agents/artifact-searcher.ts
|
|
1393
|
+
var artifactSearcherAgent = {
|
|
1394
|
+
description: "Searches past handoffs, plans, and ledgers for relevant precedent",
|
|
1395
|
+
mode: "subagent",
|
|
1396
|
+
model: "anthropic/claude-sonnet-4-20250514",
|
|
1397
|
+
temperature: 0.3,
|
|
1398
|
+
tools: {
|
|
1399
|
+
edit: false,
|
|
1400
|
+
task: false
|
|
1401
|
+
},
|
|
1402
|
+
prompt: `<purpose>
|
|
1403
|
+
Search the artifact index to find relevant past work, patterns, and lessons learned.
|
|
1404
|
+
Help the user discover precedent from previous sessions.
|
|
1405
|
+
</purpose>
|
|
1406
|
+
|
|
1407
|
+
<rules>
|
|
1408
|
+
<rule>Use artifact_search tool to query the index</rule>
|
|
1409
|
+
<rule>Explain WHY each result is relevant to the query</rule>
|
|
1410
|
+
<rule>Suggest which files to read for more detail</rule>
|
|
1411
|
+
<rule>If no results, suggest alternative search terms</rule>
|
|
1412
|
+
<rule>Highlight learnings and patterns that might apply</rule>
|
|
1413
|
+
</rules>
|
|
1414
|
+
|
|
1415
|
+
<process>
|
|
1416
|
+
<step>Understand what the user is looking for</step>
|
|
1417
|
+
<step>Formulate effective search query</step>
|
|
1418
|
+
<step>Execute search with artifact_search tool</step>
|
|
1419
|
+
<step>Analyze and explain results</step>
|
|
1420
|
+
<step>Recommend next steps (files to read, patterns to apply)</step>
|
|
1421
|
+
</process>
|
|
1422
|
+
|
|
1423
|
+
<output-format>
|
|
1424
|
+
## Search: {query}
|
|
1425
|
+
|
|
1426
|
+
### Relevant Results
|
|
1427
|
+
{For each result: explain relevance and key takeaways}
|
|
1428
|
+
|
|
1429
|
+
### Recommendations
|
|
1430
|
+
{Which files to read, patterns to consider}
|
|
1431
|
+
|
|
1432
|
+
### Alternative Searches
|
|
1433
|
+
{If results sparse, suggest other queries}
|
|
1434
|
+
</output-format>`
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1312
1437
|
// src/agents/index.ts
|
|
1313
1438
|
var agents = {
|
|
1314
1439
|
[PRIMARY_AGENT_NAME]: primaryAgent,
|
|
@@ -1322,7 +1447,9 @@ var agents = {
|
|
|
1322
1447
|
executor: executorAgent,
|
|
1323
1448
|
"handoff-creator": handoffCreatorAgent,
|
|
1324
1449
|
"handoff-resumer": handoffResumerAgent,
|
|
1325
|
-
"project-initializer": projectInitializerAgent
|
|
1450
|
+
"project-initializer": projectInitializerAgent,
|
|
1451
|
+
"ledger-creator": ledgerCreatorAgent,
|
|
1452
|
+
"artifact-searcher": artifactSearcherAgent
|
|
1326
1453
|
};
|
|
1327
1454
|
|
|
1328
1455
|
// src/tools/ast-grep/index.ts
|
|
@@ -13737,7 +13864,7 @@ function formatMatches(matches, isDryRun = false) {
|
|
|
13737
13864
|
const shown = matches.slice(0, MAX);
|
|
13738
13865
|
const lines = shown.map((m) => {
|
|
13739
13866
|
const loc = `${m.file}:${m.range.start.line}:${m.range.start.column}`;
|
|
13740
|
-
const text = m.text.length > 100 ? m.text.slice(0, 100)
|
|
13867
|
+
const text = m.text.length > 100 ? `${m.text.slice(0, 100)}...` : m.text;
|
|
13741
13868
|
if (isDryRun && m.replacement) {
|
|
13742
13869
|
return `${loc}
|
|
13743
13870
|
- ${text}
|
|
@@ -13781,16 +13908,7 @@ var ast_grep_replace = tool({
|
|
|
13781
13908
|
apply: tool.schema.boolean().optional().describe("Apply changes (default: false, dry-run)")
|
|
13782
13909
|
},
|
|
13783
13910
|
execute: async (args) => {
|
|
13784
|
-
const sgArgs = [
|
|
13785
|
-
"run",
|
|
13786
|
-
"-p",
|
|
13787
|
-
args.pattern,
|
|
13788
|
-
"-r",
|
|
13789
|
-
args.rewrite,
|
|
13790
|
-
"--lang",
|
|
13791
|
-
args.lang,
|
|
13792
|
-
"--json=compact"
|
|
13793
|
-
];
|
|
13911
|
+
const sgArgs = ["run", "-p", args.pattern, "-r", args.rewrite, "--lang", args.lang, "--json=compact"];
|
|
13794
13912
|
if (args.apply) {
|
|
13795
13913
|
sgArgs.push("--update-all");
|
|
13796
13914
|
}
|
|
@@ -13805,7 +13923,7 @@ var ast_grep_replace = tool({
|
|
|
13805
13923
|
const isDryRun = !args.apply;
|
|
13806
13924
|
const output = formatMatches(result.matches, isDryRun);
|
|
13807
13925
|
if (isDryRun && result.matches.length > 0) {
|
|
13808
|
-
return output
|
|
13926
|
+
return `${output}
|
|
13809
13927
|
|
|
13810
13928
|
(Dry run - use apply=true to apply changes)`;
|
|
13811
13929
|
}
|
|
@@ -13822,7 +13940,20 @@ import { readFileSync, statSync } from "fs";
|
|
|
13822
13940
|
import { extname, basename } from "path";
|
|
13823
13941
|
var LARGE_FILE_THRESHOLD = 100 * 1024;
|
|
13824
13942
|
var MAX_LINES_WITHOUT_EXTRACT = 200;
|
|
13825
|
-
var EXTRACTABLE_EXTENSIONS = [
|
|
13943
|
+
var EXTRACTABLE_EXTENSIONS = [
|
|
13944
|
+
".ts",
|
|
13945
|
+
".tsx",
|
|
13946
|
+
".js",
|
|
13947
|
+
".jsx",
|
|
13948
|
+
".py",
|
|
13949
|
+
".go",
|
|
13950
|
+
".rs",
|
|
13951
|
+
".java",
|
|
13952
|
+
".md",
|
|
13953
|
+
".json",
|
|
13954
|
+
".yaml",
|
|
13955
|
+
".yml"
|
|
13956
|
+
];
|
|
13826
13957
|
function extractStructure(content, ext) {
|
|
13827
13958
|
const lines = content.split(`
|
|
13828
13959
|
`);
|
|
@@ -13854,7 +13985,7 @@ function extractTypeScriptStructure(lines) {
|
|
|
13854
13985
|
const line = lines[i];
|
|
13855
13986
|
const trimmed = line.trim();
|
|
13856
13987
|
if (trimmed.startsWith("export ") || trimmed.startsWith("class ") || trimmed.startsWith("interface ") || trimmed.startsWith("type ") || trimmed.startsWith("function ") || trimmed.startsWith("const ") || trimmed.startsWith("async function ")) {
|
|
13857
|
-
const signature = trimmed.length > 80 ? trimmed.slice(0, 80)
|
|
13988
|
+
const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
|
|
13858
13989
|
output.push(`Line ${i + 1}: ${signature}`);
|
|
13859
13990
|
}
|
|
13860
13991
|
}
|
|
@@ -13868,7 +13999,7 @@ function extractPythonStructure(lines) {
|
|
|
13868
13999
|
const line = lines[i];
|
|
13869
14000
|
const trimmed = line.trim();
|
|
13870
14001
|
if (trimmed.startsWith("class ") || trimmed.startsWith("def ") || trimmed.startsWith("async def ") || trimmed.startsWith("@")) {
|
|
13871
|
-
const signature = trimmed.length > 80 ? trimmed.slice(0, 80)
|
|
14002
|
+
const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
|
|
13872
14003
|
output.push(`Line ${i + 1}: ${signature}`);
|
|
13873
14004
|
}
|
|
13874
14005
|
}
|
|
@@ -13882,7 +14013,7 @@ function extractGoStructure(lines) {
|
|
|
13882
14013
|
const line = lines[i];
|
|
13883
14014
|
const trimmed = line.trim();
|
|
13884
14015
|
if (trimmed.startsWith("type ") || trimmed.startsWith("func ") || trimmed.startsWith("package ")) {
|
|
13885
|
-
const signature = trimmed.length > 80 ? trimmed.slice(0, 80)
|
|
14016
|
+
const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
|
|
13886
14017
|
output.push(`Line ${i + 1}: ${signature}`);
|
|
13887
14018
|
}
|
|
13888
14019
|
}
|
|
@@ -13981,6 +14112,302 @@ ${content}`;
|
|
|
13981
14112
|
}
|
|
13982
14113
|
});
|
|
13983
14114
|
|
|
14115
|
+
// src/tools/artifact-index/index.ts
|
|
14116
|
+
import { Database } from "bun:sqlite";
|
|
14117
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
14118
|
+
import { join, dirname } from "path";
|
|
14119
|
+
import { mkdirSync, existsSync } from "fs";
|
|
14120
|
+
import { homedir } from "os";
|
|
14121
|
+
var DEFAULT_DB_DIR = join(homedir(), ".config", "opencode", "artifact-index");
|
|
14122
|
+
var DB_NAME = "context.db";
|
|
14123
|
+
|
|
14124
|
+
class ArtifactIndex {
|
|
14125
|
+
db = null;
|
|
14126
|
+
dbPath;
|
|
14127
|
+
constructor(dbDir = DEFAULT_DB_DIR) {
|
|
14128
|
+
this.dbPath = join(dbDir, DB_NAME);
|
|
14129
|
+
}
|
|
14130
|
+
async initialize() {
|
|
14131
|
+
const dir = dirname(this.dbPath);
|
|
14132
|
+
if (!existsSync(dir)) {
|
|
14133
|
+
mkdirSync(dir, { recursive: true });
|
|
14134
|
+
}
|
|
14135
|
+
this.db = new Database(this.dbPath);
|
|
14136
|
+
const schemaPath = join(dirname(import.meta.path), "schema.sql");
|
|
14137
|
+
let schema;
|
|
14138
|
+
try {
|
|
14139
|
+
schema = readFileSync2(schemaPath, "utf-8");
|
|
14140
|
+
} catch {
|
|
14141
|
+
schema = this.getInlineSchema();
|
|
14142
|
+
}
|
|
14143
|
+
this.db.exec(schema);
|
|
14144
|
+
}
|
|
14145
|
+
getInlineSchema() {
|
|
14146
|
+
return `
|
|
14147
|
+
CREATE TABLE IF NOT EXISTS handoffs (
|
|
14148
|
+
id TEXT PRIMARY KEY,
|
|
14149
|
+
session_name TEXT,
|
|
14150
|
+
file_path TEXT UNIQUE NOT NULL,
|
|
14151
|
+
task_summary TEXT,
|
|
14152
|
+
what_worked TEXT,
|
|
14153
|
+
what_failed TEXT,
|
|
14154
|
+
learnings TEXT,
|
|
14155
|
+
outcome TEXT,
|
|
14156
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
14157
|
+
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
14158
|
+
);
|
|
14159
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
14160
|
+
id TEXT PRIMARY KEY,
|
|
14161
|
+
title TEXT,
|
|
14162
|
+
file_path TEXT UNIQUE NOT NULL,
|
|
14163
|
+
overview TEXT,
|
|
14164
|
+
approach TEXT,
|
|
14165
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
14166
|
+
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
14167
|
+
);
|
|
14168
|
+
CREATE TABLE IF NOT EXISTS ledgers (
|
|
14169
|
+
id TEXT PRIMARY KEY,
|
|
14170
|
+
session_name TEXT,
|
|
14171
|
+
file_path TEXT UNIQUE NOT NULL,
|
|
14172
|
+
goal TEXT,
|
|
14173
|
+
state_now TEXT,
|
|
14174
|
+
key_decisions TEXT,
|
|
14175
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
14176
|
+
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
14177
|
+
);
|
|
14178
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS handoffs_fts USING fts5(id, session_name, task_summary, what_worked, what_failed, learnings);
|
|
14179
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5(id, title, overview, approach);
|
|
14180
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS ledgers_fts USING fts5(id, session_name, goal, state_now, key_decisions);
|
|
14181
|
+
`;
|
|
14182
|
+
}
|
|
14183
|
+
async indexHandoff(record2) {
|
|
14184
|
+
if (!this.db)
|
|
14185
|
+
throw new Error("Database not initialized");
|
|
14186
|
+
const existing = this.db.query(`SELECT id FROM handoffs WHERE file_path = ?`).get(record2.filePath);
|
|
14187
|
+
if (existing) {
|
|
14188
|
+
this.db.run(`DELETE FROM handoffs_fts WHERE id = ?`, [existing.id]);
|
|
14189
|
+
}
|
|
14190
|
+
this.db.run(`
|
|
14191
|
+
INSERT INTO handoffs (id, session_name, file_path, task_summary, what_worked, what_failed, learnings, outcome, indexed_at)
|
|
14192
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
14193
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
14194
|
+
id = excluded.id,
|
|
14195
|
+
session_name = excluded.session_name,
|
|
14196
|
+
task_summary = excluded.task_summary,
|
|
14197
|
+
what_worked = excluded.what_worked,
|
|
14198
|
+
what_failed = excluded.what_failed,
|
|
14199
|
+
learnings = excluded.learnings,
|
|
14200
|
+
outcome = excluded.outcome,
|
|
14201
|
+
indexed_at = CURRENT_TIMESTAMP
|
|
14202
|
+
`, [
|
|
14203
|
+
record2.id,
|
|
14204
|
+
record2.sessionName ?? null,
|
|
14205
|
+
record2.filePath,
|
|
14206
|
+
record2.taskSummary ?? null,
|
|
14207
|
+
record2.whatWorked ?? null,
|
|
14208
|
+
record2.whatFailed ?? null,
|
|
14209
|
+
record2.learnings ?? null,
|
|
14210
|
+
record2.outcome ?? null
|
|
14211
|
+
]);
|
|
14212
|
+
this.db.run(`
|
|
14213
|
+
INSERT INTO handoffs_fts (id, session_name, task_summary, what_worked, what_failed, learnings)
|
|
14214
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
14215
|
+
`, [
|
|
14216
|
+
record2.id,
|
|
14217
|
+
record2.sessionName ?? null,
|
|
14218
|
+
record2.taskSummary ?? null,
|
|
14219
|
+
record2.whatWorked ?? null,
|
|
14220
|
+
record2.whatFailed ?? null,
|
|
14221
|
+
record2.learnings ?? null
|
|
14222
|
+
]);
|
|
14223
|
+
}
|
|
14224
|
+
async indexPlan(record2) {
|
|
14225
|
+
if (!this.db)
|
|
14226
|
+
throw new Error("Database not initialized");
|
|
14227
|
+
const existing = this.db.query(`SELECT id FROM plans WHERE file_path = ?`).get(record2.filePath);
|
|
14228
|
+
if (existing) {
|
|
14229
|
+
this.db.run(`DELETE FROM plans_fts WHERE id = ?`, [existing.id]);
|
|
14230
|
+
}
|
|
14231
|
+
this.db.run(`
|
|
14232
|
+
INSERT INTO plans (id, title, file_path, overview, approach, indexed_at)
|
|
14233
|
+
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
14234
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
14235
|
+
id = excluded.id,
|
|
14236
|
+
title = excluded.title,
|
|
14237
|
+
overview = excluded.overview,
|
|
14238
|
+
approach = excluded.approach,
|
|
14239
|
+
indexed_at = CURRENT_TIMESTAMP
|
|
14240
|
+
`, [record2.id, record2.title ?? null, record2.filePath, record2.overview ?? null, record2.approach ?? null]);
|
|
14241
|
+
this.db.run(`
|
|
14242
|
+
INSERT INTO plans_fts (id, title, overview, approach)
|
|
14243
|
+
VALUES (?, ?, ?, ?)
|
|
14244
|
+
`, [record2.id, record2.title ?? null, record2.overview ?? null, record2.approach ?? null]);
|
|
14245
|
+
}
|
|
14246
|
+
async indexLedger(record2) {
|
|
14247
|
+
if (!this.db)
|
|
14248
|
+
throw new Error("Database not initialized");
|
|
14249
|
+
const existing = this.db.query(`SELECT id FROM ledgers WHERE file_path = ?`).get(record2.filePath);
|
|
14250
|
+
if (existing) {
|
|
14251
|
+
this.db.run(`DELETE FROM ledgers_fts WHERE id = ?`, [existing.id]);
|
|
14252
|
+
}
|
|
14253
|
+
this.db.run(`
|
|
14254
|
+
INSERT INTO ledgers (id, session_name, file_path, goal, state_now, key_decisions, indexed_at)
|
|
14255
|
+
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
14256
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
14257
|
+
id = excluded.id,
|
|
14258
|
+
session_name = excluded.session_name,
|
|
14259
|
+
goal = excluded.goal,
|
|
14260
|
+
state_now = excluded.state_now,
|
|
14261
|
+
key_decisions = excluded.key_decisions,
|
|
14262
|
+
indexed_at = CURRENT_TIMESTAMP
|
|
14263
|
+
`, [
|
|
14264
|
+
record2.id,
|
|
14265
|
+
record2.sessionName ?? null,
|
|
14266
|
+
record2.filePath,
|
|
14267
|
+
record2.goal ?? null,
|
|
14268
|
+
record2.stateNow ?? null,
|
|
14269
|
+
record2.keyDecisions ?? null
|
|
14270
|
+
]);
|
|
14271
|
+
this.db.run(`
|
|
14272
|
+
INSERT INTO ledgers_fts (id, session_name, goal, state_now, key_decisions)
|
|
14273
|
+
VALUES (?, ?, ?, ?, ?)
|
|
14274
|
+
`, [
|
|
14275
|
+
record2.id,
|
|
14276
|
+
record2.sessionName ?? null,
|
|
14277
|
+
record2.goal ?? null,
|
|
14278
|
+
record2.stateNow ?? null,
|
|
14279
|
+
record2.keyDecisions ?? null
|
|
14280
|
+
]);
|
|
14281
|
+
}
|
|
14282
|
+
async search(query, limit = 10) {
|
|
14283
|
+
if (!this.db)
|
|
14284
|
+
throw new Error("Database not initialized");
|
|
14285
|
+
const results = [];
|
|
14286
|
+
const escapedQuery = this.escapeFtsQuery(query);
|
|
14287
|
+
const handoffs = this.db.query(`
|
|
14288
|
+
SELECT h.id, h.file_path, h.task_summary, rank
|
|
14289
|
+
FROM handoffs_fts
|
|
14290
|
+
JOIN handoffs h ON handoffs_fts.id = h.id
|
|
14291
|
+
WHERE handoffs_fts MATCH ?
|
|
14292
|
+
ORDER BY rank
|
|
14293
|
+
LIMIT ?
|
|
14294
|
+
`).all(escapedQuery, limit);
|
|
14295
|
+
for (const row of handoffs) {
|
|
14296
|
+
results.push({
|
|
14297
|
+
type: "handoff",
|
|
14298
|
+
id: row.id,
|
|
14299
|
+
filePath: row.file_path,
|
|
14300
|
+
summary: row.task_summary,
|
|
14301
|
+
score: -row.rank
|
|
14302
|
+
});
|
|
14303
|
+
}
|
|
14304
|
+
const plans = this.db.query(`
|
|
14305
|
+
SELECT p.id, p.file_path, p.title, rank
|
|
14306
|
+
FROM plans_fts
|
|
14307
|
+
JOIN plans p ON plans_fts.id = p.id
|
|
14308
|
+
WHERE plans_fts MATCH ?
|
|
14309
|
+
ORDER BY rank
|
|
14310
|
+
LIMIT ?
|
|
14311
|
+
`).all(escapedQuery, limit);
|
|
14312
|
+
for (const row of plans) {
|
|
14313
|
+
results.push({
|
|
14314
|
+
type: "plan",
|
|
14315
|
+
id: row.id,
|
|
14316
|
+
filePath: row.file_path,
|
|
14317
|
+
title: row.title,
|
|
14318
|
+
score: -row.rank
|
|
14319
|
+
});
|
|
14320
|
+
}
|
|
14321
|
+
const ledgers = this.db.query(`
|
|
14322
|
+
SELECT l.id, l.file_path, l.session_name, l.goal, rank
|
|
14323
|
+
FROM ledgers_fts
|
|
14324
|
+
JOIN ledgers l ON ledgers_fts.id = l.id
|
|
14325
|
+
WHERE ledgers_fts MATCH ?
|
|
14326
|
+
ORDER BY rank
|
|
14327
|
+
LIMIT ?
|
|
14328
|
+
`).all(escapedQuery, limit);
|
|
14329
|
+
for (const row of ledgers) {
|
|
14330
|
+
results.push({
|
|
14331
|
+
type: "ledger",
|
|
14332
|
+
id: row.id,
|
|
14333
|
+
filePath: row.file_path,
|
|
14334
|
+
title: row.session_name,
|
|
14335
|
+
summary: row.goal,
|
|
14336
|
+
score: -row.rank
|
|
14337
|
+
});
|
|
14338
|
+
}
|
|
14339
|
+
results.sort((a, b) => b.score - a.score);
|
|
14340
|
+
return results.slice(0, limit);
|
|
14341
|
+
}
|
|
14342
|
+
escapeFtsQuery(query) {
|
|
14343
|
+
return query.replace(/['"]/g, "").split(/\s+/).filter((term) => term.length > 0).map((term) => `"${term}"`).join(" OR ");
|
|
14344
|
+
}
|
|
14345
|
+
async close() {
|
|
14346
|
+
if (this.db) {
|
|
14347
|
+
this.db.close();
|
|
14348
|
+
this.db = null;
|
|
14349
|
+
}
|
|
14350
|
+
}
|
|
14351
|
+
}
|
|
14352
|
+
var globalIndex = null;
|
|
14353
|
+
async function getArtifactIndex() {
|
|
14354
|
+
if (!globalIndex) {
|
|
14355
|
+
globalIndex = new ArtifactIndex;
|
|
14356
|
+
await globalIndex.initialize();
|
|
14357
|
+
}
|
|
14358
|
+
return globalIndex;
|
|
14359
|
+
}
|
|
14360
|
+
|
|
14361
|
+
// src/tools/artifact-search.ts
|
|
14362
|
+
var artifact_search = tool({
|
|
14363
|
+
description: `Search past handoffs, plans, and ledgers for relevant precedent.
|
|
14364
|
+
Use this to find:
|
|
14365
|
+
- Similar problems you've solved before
|
|
14366
|
+
- Patterns and approaches that worked
|
|
14367
|
+
- Lessons learned from past sessions
|
|
14368
|
+
Returns ranked results with file paths for further reading.`,
|
|
14369
|
+
args: {
|
|
14370
|
+
query: tool.schema.string().describe("Search query - describe what you're looking for"),
|
|
14371
|
+
limit: tool.schema.number().optional().describe("Max results to return (default: 10)"),
|
|
14372
|
+
type: tool.schema.enum(["all", "handoff", "plan", "ledger"]).optional().describe("Filter by artifact type (default: all)")
|
|
14373
|
+
},
|
|
14374
|
+
execute: async (args) => {
|
|
14375
|
+
try {
|
|
14376
|
+
const index = await getArtifactIndex();
|
|
14377
|
+
const results = await index.search(args.query, args.limit || 10);
|
|
14378
|
+
const filtered = args.type && args.type !== "all" ? results.filter((r) => r.type === args.type) : results;
|
|
14379
|
+
if (filtered.length === 0) {
|
|
14380
|
+
return `No results found for "${args.query}". Try broader search terms.`;
|
|
14381
|
+
}
|
|
14382
|
+
let output = `## Search Results for "${args.query}"
|
|
14383
|
+
|
|
14384
|
+
`;
|
|
14385
|
+
output += `Found ${filtered.length} result(s):
|
|
14386
|
+
|
|
14387
|
+
`;
|
|
14388
|
+
for (const result of filtered) {
|
|
14389
|
+
const typeLabel = result.type.charAt(0).toUpperCase() + result.type.slice(1);
|
|
14390
|
+
output += `### ${typeLabel}: ${result.title || result.id}
|
|
14391
|
+
`;
|
|
14392
|
+
output += `**File:** \`${result.filePath}\`
|
|
14393
|
+
`;
|
|
14394
|
+
if (result.summary) {
|
|
14395
|
+
output += `**Summary:** ${result.summary}
|
|
14396
|
+
`;
|
|
14397
|
+
}
|
|
14398
|
+
output += `**Relevance Score:** ${result.score.toFixed(2)}
|
|
14399
|
+
|
|
14400
|
+
`;
|
|
14401
|
+
}
|
|
14402
|
+
output += `---
|
|
14403
|
+
*Use the Read tool to view full content of relevant files.*`;
|
|
14404
|
+
return output;
|
|
14405
|
+
} catch (e) {
|
|
14406
|
+
return `Error searching artifacts: ${e instanceof Error ? e.message : String(e)}`;
|
|
14407
|
+
}
|
|
14408
|
+
}
|
|
14409
|
+
});
|
|
14410
|
+
|
|
13984
14411
|
// src/hooks/auto-compact.ts
|
|
13985
14412
|
function parseTokenLimitError(error45) {
|
|
13986
14413
|
if (!error45)
|
|
@@ -14076,9 +14503,9 @@ function createAutoCompactHook(ctx) {
|
|
|
14076
14503
|
}
|
|
14077
14504
|
}).catch(() => {});
|
|
14078
14505
|
}
|
|
14079
|
-
} catch (
|
|
14506
|
+
} catch (_e) {
|
|
14080
14507
|
state.retryCount.set(sessionID, retries + 1);
|
|
14081
|
-
const delay = Math.min(1000 *
|
|
14508
|
+
const delay = Math.min(1000 * 2 ** retries, 1e4);
|
|
14082
14509
|
setTimeout(() => {
|
|
14083
14510
|
state.inProgress.delete(sessionID);
|
|
14084
14511
|
attemptRecovery(sessionID, providerID, modelID);
|
|
@@ -14160,14 +14587,8 @@ function createAutoCompactHook(ctx) {
|
|
|
14160
14587
|
|
|
14161
14588
|
// src/hooks/context-injector.ts
|
|
14162
14589
|
import { readFile } from "fs/promises";
|
|
14163
|
-
import { join, dirname, resolve } from "path";
|
|
14164
|
-
var ROOT_CONTEXT_FILES = [
|
|
14165
|
-
"ARCHITECTURE.md",
|
|
14166
|
-
"CODE_STYLE.md",
|
|
14167
|
-
"AGENTS.md",
|
|
14168
|
-
"CLAUDE.md",
|
|
14169
|
-
"README.md"
|
|
14170
|
-
];
|
|
14590
|
+
import { join as join2, dirname as dirname2, resolve } from "path";
|
|
14591
|
+
var ROOT_CONTEXT_FILES = ["ARCHITECTURE.md", "CODE_STYLE.md", "AGENTS.md", "CLAUDE.md", "README.md"];
|
|
14171
14592
|
var DIRECTORY_CONTEXT_FILES = ["AGENTS.md", "README.md"];
|
|
14172
14593
|
var FILE_ACCESS_TOOLS = ["Read", "read", "Edit", "edit"];
|
|
14173
14594
|
var CACHE_TTL = 30000;
|
|
@@ -14186,7 +14607,7 @@ function createContextInjectorHook(ctx) {
|
|
|
14186
14607
|
cache.lastRootCheck = now;
|
|
14187
14608
|
for (const filename of ROOT_CONTEXT_FILES) {
|
|
14188
14609
|
try {
|
|
14189
|
-
const filepath =
|
|
14610
|
+
const filepath = join2(ctx.directory, filename);
|
|
14190
14611
|
const content = await readFile(filepath, "utf-8");
|
|
14191
14612
|
if (content.trim()) {
|
|
14192
14613
|
cache.rootContent.set(filename, content);
|
|
@@ -14198,15 +14619,15 @@ function createContextInjectorHook(ctx) {
|
|
|
14198
14619
|
async function walkUpForContextFiles(filePath) {
|
|
14199
14620
|
const absPath = resolve(filePath);
|
|
14200
14621
|
const projectRoot = resolve(ctx.directory);
|
|
14201
|
-
const cacheKey =
|
|
14622
|
+
const cacheKey = dirname2(absPath);
|
|
14202
14623
|
if (cache.directoryContent.has(cacheKey)) {
|
|
14203
14624
|
return cache.directoryContent.get(cacheKey);
|
|
14204
14625
|
}
|
|
14205
14626
|
const collected = new Map;
|
|
14206
|
-
let currentDir =
|
|
14627
|
+
let currentDir = dirname2(absPath);
|
|
14207
14628
|
while (currentDir.startsWith(projectRoot) || currentDir === projectRoot) {
|
|
14208
14629
|
for (const filename of DIRECTORY_CONTEXT_FILES) {
|
|
14209
|
-
const contextPath =
|
|
14630
|
+
const contextPath = join2(currentDir, filename);
|
|
14210
14631
|
const relPath = currentDir.replace(projectRoot, "").replace(/^\//, "") || ".";
|
|
14211
14632
|
const key = `${relPath}/${filename}`;
|
|
14212
14633
|
if (!collected.has(key)) {
|
|
@@ -14220,7 +14641,7 @@ function createContextInjectorHook(ctx) {
|
|
|
14220
14641
|
}
|
|
14221
14642
|
if (currentDir === projectRoot)
|
|
14222
14643
|
break;
|
|
14223
|
-
const parent =
|
|
14644
|
+
const parent = dirname2(currentDir);
|
|
14224
14645
|
if (parent === currentDir)
|
|
14225
14646
|
break;
|
|
14226
14647
|
currentDir = parent;
|
|
@@ -14281,144 +14702,6 @@ ${blocks.join(`
|
|
|
14281
14702
|
};
|
|
14282
14703
|
}
|
|
14283
14704
|
|
|
14284
|
-
// src/hooks/preemptive-compaction.ts
|
|
14285
|
-
var MODEL_CONTEXT_LIMITS = {
|
|
14286
|
-
"claude-opus": 200000,
|
|
14287
|
-
"claude-sonnet": 200000,
|
|
14288
|
-
"claude-haiku": 200000,
|
|
14289
|
-
"claude-3": 200000,
|
|
14290
|
-
"claude-4": 200000,
|
|
14291
|
-
"gpt-4o": 128000,
|
|
14292
|
-
"gpt-4-turbo": 128000,
|
|
14293
|
-
"gpt-4": 128000,
|
|
14294
|
-
"gpt-5": 200000,
|
|
14295
|
-
o1: 200000,
|
|
14296
|
-
o3: 200000,
|
|
14297
|
-
gemini: 1e6
|
|
14298
|
-
};
|
|
14299
|
-
var DEFAULT_CONTEXT_LIMIT = 200000;
|
|
14300
|
-
var DEFAULT_THRESHOLD = 0.8;
|
|
14301
|
-
var MIN_TOKENS_FOR_COMPACTION = 50000;
|
|
14302
|
-
var COMPACTION_COOLDOWN_MS = 60000;
|
|
14303
|
-
function getContextLimit(modelID) {
|
|
14304
|
-
const modelLower = modelID.toLowerCase();
|
|
14305
|
-
for (const [pattern, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
|
|
14306
|
-
if (modelLower.includes(pattern)) {
|
|
14307
|
-
return limit;
|
|
14308
|
-
}
|
|
14309
|
-
}
|
|
14310
|
-
return DEFAULT_CONTEXT_LIMIT;
|
|
14311
|
-
}
|
|
14312
|
-
function createPreemptiveCompactionHook(ctx) {
|
|
14313
|
-
const state = {
|
|
14314
|
-
lastCompactionTime: new Map,
|
|
14315
|
-
compactionInProgress: new Set
|
|
14316
|
-
};
|
|
14317
|
-
async function checkAndCompact(sessionID, providerID, modelID) {
|
|
14318
|
-
if (state.compactionInProgress.has(sessionID))
|
|
14319
|
-
return;
|
|
14320
|
-
const lastTime = state.lastCompactionTime.get(sessionID) || 0;
|
|
14321
|
-
if (Date.now() - lastTime < COMPACTION_COOLDOWN_MS)
|
|
14322
|
-
return;
|
|
14323
|
-
try {
|
|
14324
|
-
const resp = await ctx.client.session.messages({
|
|
14325
|
-
path: { id: sessionID },
|
|
14326
|
-
query: { directory: ctx.directory }
|
|
14327
|
-
});
|
|
14328
|
-
const messages = resp.data;
|
|
14329
|
-
if (!Array.isArray(messages) || messages.length === 0)
|
|
14330
|
-
return;
|
|
14331
|
-
const lastAssistant = [...messages].reverse().find((m) => {
|
|
14332
|
-
const msg = m;
|
|
14333
|
-
const info2 = msg.info;
|
|
14334
|
-
return info2?.role === "assistant";
|
|
14335
|
-
});
|
|
14336
|
-
if (!lastAssistant)
|
|
14337
|
-
return;
|
|
14338
|
-
const info = lastAssistant.info;
|
|
14339
|
-
const usage = info?.usage;
|
|
14340
|
-
const inputTokens = usage?.inputTokens || 0;
|
|
14341
|
-
const cacheRead = usage?.cacheReadInputTokens || 0;
|
|
14342
|
-
const totalUsed = inputTokens + cacheRead;
|
|
14343
|
-
if (totalUsed < MIN_TOKENS_FOR_COMPACTION)
|
|
14344
|
-
return;
|
|
14345
|
-
const model = modelID || info?.modelID || "";
|
|
14346
|
-
const contextLimit = getContextLimit(model);
|
|
14347
|
-
const usageRatio = totalUsed / contextLimit;
|
|
14348
|
-
if (usageRatio < DEFAULT_THRESHOLD)
|
|
14349
|
-
return;
|
|
14350
|
-
const lastUserMsg = [...messages].reverse().find((m) => {
|
|
14351
|
-
const msg = m;
|
|
14352
|
-
const msgInfo = msg.info;
|
|
14353
|
-
return msgInfo?.role === "user";
|
|
14354
|
-
});
|
|
14355
|
-
if (lastUserMsg) {
|
|
14356
|
-
const parts = lastUserMsg.parts;
|
|
14357
|
-
const text = parts?.find((p) => p.type === "text")?.text || "";
|
|
14358
|
-
if (text.includes("summarized") || text.includes("compacted"))
|
|
14359
|
-
return;
|
|
14360
|
-
}
|
|
14361
|
-
state.compactionInProgress.add(sessionID);
|
|
14362
|
-
state.lastCompactionTime.set(sessionID, Date.now());
|
|
14363
|
-
await ctx.client.tui.showToast({
|
|
14364
|
-
body: {
|
|
14365
|
-
title: "Context Window",
|
|
14366
|
-
message: `${Math.round(usageRatio * 100)}% used - auto-compacting...`,
|
|
14367
|
-
variant: "warning",
|
|
14368
|
-
duration: 3000
|
|
14369
|
-
}
|
|
14370
|
-
}).catch(() => {});
|
|
14371
|
-
const provider = providerID || info?.providerID;
|
|
14372
|
-
const modelToUse = modelID || info?.modelID;
|
|
14373
|
-
if (provider && modelToUse) {
|
|
14374
|
-
await ctx.client.session.summarize({
|
|
14375
|
-
path: { id: sessionID },
|
|
14376
|
-
body: { providerID: provider, modelID: modelToUse },
|
|
14377
|
-
query: { directory: ctx.directory }
|
|
14378
|
-
});
|
|
14379
|
-
await ctx.client.tui.showToast({
|
|
14380
|
-
body: {
|
|
14381
|
-
title: "Compacted",
|
|
14382
|
-
message: "Session summarized successfully",
|
|
14383
|
-
variant: "success",
|
|
14384
|
-
duration: 3000
|
|
14385
|
-
}
|
|
14386
|
-
}).catch(() => {});
|
|
14387
|
-
}
|
|
14388
|
-
} catch (e) {} finally {
|
|
14389
|
-
state.compactionInProgress.delete(sessionID);
|
|
14390
|
-
}
|
|
14391
|
-
}
|
|
14392
|
-
return {
|
|
14393
|
-
event: async ({ event }) => {
|
|
14394
|
-
const props = event.properties;
|
|
14395
|
-
if (event.type === "session.deleted") {
|
|
14396
|
-
const sessionInfo = props?.info;
|
|
14397
|
-
if (sessionInfo?.id) {
|
|
14398
|
-
state.lastCompactionTime.delete(sessionInfo.id);
|
|
14399
|
-
state.compactionInProgress.delete(sessionInfo.id);
|
|
14400
|
-
}
|
|
14401
|
-
return;
|
|
14402
|
-
}
|
|
14403
|
-
if (event.type === "message.updated") {
|
|
14404
|
-
const info = props?.info;
|
|
14405
|
-
const sessionID = info?.sessionID;
|
|
14406
|
-
if (sessionID && info?.role === "assistant") {
|
|
14407
|
-
const providerID = info.providerID;
|
|
14408
|
-
const modelID = info.modelID;
|
|
14409
|
-
await checkAndCompact(sessionID, providerID, modelID);
|
|
14410
|
-
}
|
|
14411
|
-
}
|
|
14412
|
-
if (event.type === "session.idle") {
|
|
14413
|
-
const sessionID = props?.sessionID;
|
|
14414
|
-
if (sessionID) {
|
|
14415
|
-
await checkAndCompact(sessionID);
|
|
14416
|
-
}
|
|
14417
|
-
}
|
|
14418
|
-
}
|
|
14419
|
-
};
|
|
14420
|
-
}
|
|
14421
|
-
|
|
14422
14705
|
// src/hooks/session-recovery.ts
|
|
14423
14706
|
var RECOVERABLE_ERRORS = {
|
|
14424
14707
|
TOOL_RESULT_MISSING: "tool_result block(s) missing",
|
|
@@ -14601,15 +14884,9 @@ function createSessionRecoveryHook(ctx) {
|
|
|
14601
14884
|
}
|
|
14602
14885
|
|
|
14603
14886
|
// src/hooks/token-aware-truncation.ts
|
|
14604
|
-
var TRUNCATABLE_TOOLS = [
|
|
14605
|
-
"grep",
|
|
14606
|
-
"Grep",
|
|
14607
|
-
"glob",
|
|
14608
|
-
"Glob",
|
|
14609
|
-
"ast_grep_search"
|
|
14610
|
-
];
|
|
14887
|
+
var TRUNCATABLE_TOOLS = ["grep", "Grep", "glob", "Glob", "ast_grep_search"];
|
|
14611
14888
|
var CHARS_PER_TOKEN = 4;
|
|
14612
|
-
var
|
|
14889
|
+
var DEFAULT_CONTEXT_LIMIT = 200000;
|
|
14613
14890
|
var DEFAULT_MAX_OUTPUT_TOKENS = 50000;
|
|
14614
14891
|
var SAFETY_MARGIN = 0.5;
|
|
14615
14892
|
var PRESERVE_HEADER_LINES = 3;
|
|
@@ -14670,7 +14947,7 @@ function createTokenAwareTruncationHook(ctx) {
|
|
|
14670
14947
|
});
|
|
14671
14948
|
const messages = resp.data;
|
|
14672
14949
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
14673
|
-
return { used: 0, limit:
|
|
14950
|
+
return { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
|
|
14674
14951
|
}
|
|
14675
14952
|
const lastAssistant = [...messages].reverse().find((m) => {
|
|
14676
14953
|
const msg = m;
|
|
@@ -14678,19 +14955,19 @@ function createTokenAwareTruncationHook(ctx) {
|
|
|
14678
14955
|
return info2?.role === "assistant";
|
|
14679
14956
|
});
|
|
14680
14957
|
if (!lastAssistant) {
|
|
14681
|
-
return { used: 0, limit:
|
|
14958
|
+
return { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
|
|
14682
14959
|
}
|
|
14683
14960
|
const info = lastAssistant.info;
|
|
14684
14961
|
const usage = info?.usage;
|
|
14685
14962
|
const inputTokens = usage?.inputTokens || 0;
|
|
14686
14963
|
const cacheRead = usage?.cacheReadInputTokens || 0;
|
|
14687
14964
|
const used = inputTokens + cacheRead;
|
|
14688
|
-
const limit =
|
|
14965
|
+
const limit = DEFAULT_CONTEXT_LIMIT;
|
|
14689
14966
|
const result = { used, limit };
|
|
14690
14967
|
state.sessionTokenUsage.set(sessionID, result);
|
|
14691
14968
|
return result;
|
|
14692
14969
|
} catch {
|
|
14693
|
-
return state.sessionTokenUsage.get(sessionID) || { used: 0, limit:
|
|
14970
|
+
return state.sessionTokenUsage.get(sessionID) || { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
|
|
14694
14971
|
}
|
|
14695
14972
|
}
|
|
14696
14973
|
function calculateMaxOutputTokens(used, limit) {
|
|
@@ -14750,8 +15027,8 @@ function createTokenAwareTruncationHook(ctx) {
|
|
|
14750
15027
|
// src/hooks/context-window-monitor.ts
|
|
14751
15028
|
var WARNING_THRESHOLD = 0.7;
|
|
14752
15029
|
var CRITICAL_THRESHOLD = 0.85;
|
|
14753
|
-
var
|
|
14754
|
-
var
|
|
15030
|
+
var DEFAULT_CONTEXT_LIMIT2 = 200000;
|
|
15031
|
+
var MODEL_CONTEXT_LIMITS = {
|
|
14755
15032
|
"claude-opus": 200000,
|
|
14756
15033
|
"claude-sonnet": 200000,
|
|
14757
15034
|
"claude-haiku": 200000,
|
|
@@ -14761,14 +15038,14 @@ var MODEL_CONTEXT_LIMITS2 = {
|
|
|
14761
15038
|
o3: 200000,
|
|
14762
15039
|
gemini: 1e6
|
|
14763
15040
|
};
|
|
14764
|
-
function
|
|
15041
|
+
function getContextLimit(modelID) {
|
|
14765
15042
|
const modelLower = modelID.toLowerCase();
|
|
14766
|
-
for (const [pattern, limit] of Object.entries(
|
|
15043
|
+
for (const [pattern, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
|
|
14767
15044
|
if (modelLower.includes(pattern)) {
|
|
14768
15045
|
return limit;
|
|
14769
15046
|
}
|
|
14770
15047
|
}
|
|
14771
|
-
return
|
|
15048
|
+
return DEFAULT_CONTEXT_LIMIT2;
|
|
14772
15049
|
}
|
|
14773
15050
|
var WARNING_COOLDOWN_MS = 120000;
|
|
14774
15051
|
function createContextWindowMonitorHook(ctx) {
|
|
@@ -14818,7 +15095,7 @@ function createContextWindowMonitorHook(ctx) {
|
|
|
14818
15095
|
const cacheRead = usage?.cacheReadInputTokens || 0;
|
|
14819
15096
|
const totalUsed = inputTokens + cacheRead;
|
|
14820
15097
|
const modelID = info.modelID || "";
|
|
14821
|
-
const contextLimit =
|
|
15098
|
+
const contextLimit = getContextLimit(modelID);
|
|
14822
15099
|
const usageRatio = totalUsed / contextLimit;
|
|
14823
15100
|
state.lastUsageRatio.set(sessionID, usageRatio);
|
|
14824
15101
|
if (usageRatio >= WARNING_THRESHOLD) {
|
|
@@ -14900,7 +15177,7 @@ function analyzeComments(content) {
|
|
|
14900
15177
|
}
|
|
14901
15178
|
return issues;
|
|
14902
15179
|
}
|
|
14903
|
-
function createCommentCheckerHook(
|
|
15180
|
+
function createCommentCheckerHook(_ctx) {
|
|
14904
15181
|
return {
|
|
14905
15182
|
"tool.execute.after": async (input, output) => {
|
|
14906
15183
|
if (input.tool !== "Edit" && input.tool !== "edit")
|
|
@@ -14926,6 +15203,373 @@ Comments should explain WHY, not WHAT. Consider removing obvious comments.`;
|
|
|
14926
15203
|
};
|
|
14927
15204
|
}
|
|
14928
15205
|
|
|
15206
|
+
// src/hooks/ledger-loader.ts
|
|
15207
|
+
import { readFile as readFile2, readdir } from "fs/promises";
|
|
15208
|
+
import { join as join3 } from "path";
|
|
15209
|
+
var LEDGER_DIR = "thoughts/ledgers";
|
|
15210
|
+
var LEDGER_PREFIX = "CONTINUITY_";
|
|
15211
|
+
async function findCurrentLedger(directory) {
|
|
15212
|
+
const ledgerDir = join3(directory, LEDGER_DIR);
|
|
15213
|
+
try {
|
|
15214
|
+
const files = await readdir(ledgerDir);
|
|
15215
|
+
const ledgerFiles = files.filter((f) => f.startsWith(LEDGER_PREFIX) && f.endsWith(".md"));
|
|
15216
|
+
if (ledgerFiles.length === 0)
|
|
15217
|
+
return null;
|
|
15218
|
+
let latestFile = ledgerFiles[0];
|
|
15219
|
+
let latestMtime = 0;
|
|
15220
|
+
for (const file2 of ledgerFiles) {
|
|
15221
|
+
const filePath2 = join3(ledgerDir, file2);
|
|
15222
|
+
try {
|
|
15223
|
+
const stat = await Bun.file(filePath2).stat();
|
|
15224
|
+
if (stat && stat.mtime.getTime() > latestMtime) {
|
|
15225
|
+
latestMtime = stat.mtime.getTime();
|
|
15226
|
+
latestFile = file2;
|
|
15227
|
+
}
|
|
15228
|
+
} catch {}
|
|
15229
|
+
}
|
|
15230
|
+
const filePath = join3(ledgerDir, latestFile);
|
|
15231
|
+
const content = await readFile2(filePath, "utf-8");
|
|
15232
|
+
const sessionName = latestFile.replace(LEDGER_PREFIX, "").replace(".md", "");
|
|
15233
|
+
return { sessionName, filePath, content };
|
|
15234
|
+
} catch {
|
|
15235
|
+
return null;
|
|
15236
|
+
}
|
|
15237
|
+
}
|
|
15238
|
+
function formatLedgerInjection(ledger) {
|
|
15239
|
+
return `<continuity-ledger session="${ledger.sessionName}">
|
|
15240
|
+
${ledger.content}
|
|
15241
|
+
</continuity-ledger>
|
|
15242
|
+
|
|
15243
|
+
You are resuming work from a previous context clear. The ledger above contains your session state.
|
|
15244
|
+
Review it and continue from where you left off. The "Now" item is your current focus.`;
|
|
15245
|
+
}
|
|
15246
|
+
function createLedgerLoaderHook(ctx) {
|
|
15247
|
+
return {
|
|
15248
|
+
"chat.params": async (_input, output) => {
|
|
15249
|
+
const ledger = await findCurrentLedger(ctx.directory);
|
|
15250
|
+
if (!ledger)
|
|
15251
|
+
return;
|
|
15252
|
+
const injection = formatLedgerInjection(ledger);
|
|
15253
|
+
if (output.system) {
|
|
15254
|
+
output.system = `${injection}
|
|
15255
|
+
|
|
15256
|
+
${output.system}`;
|
|
15257
|
+
} else {
|
|
15258
|
+
output.system = injection;
|
|
15259
|
+
}
|
|
15260
|
+
}
|
|
15261
|
+
};
|
|
15262
|
+
}
|
|
15263
|
+
|
|
15264
|
+
// src/hooks/auto-clear-ledger.ts
|
|
15265
|
+
var MODEL_CONTEXT_LIMITS2 = {
|
|
15266
|
+
"claude-opus": 200000,
|
|
15267
|
+
"claude-sonnet": 200000,
|
|
15268
|
+
"claude-haiku": 200000,
|
|
15269
|
+
"claude-3": 200000,
|
|
15270
|
+
"claude-4": 200000,
|
|
15271
|
+
"gpt-4o": 128000,
|
|
15272
|
+
"gpt-4-turbo": 128000,
|
|
15273
|
+
"gpt-4": 128000,
|
|
15274
|
+
"gpt-5": 200000,
|
|
15275
|
+
o1: 200000,
|
|
15276
|
+
o3: 200000,
|
|
15277
|
+
gemini: 1e6
|
|
15278
|
+
};
|
|
15279
|
+
var DEFAULT_CONTEXT_LIMIT3 = 200000;
|
|
15280
|
+
var DEFAULT_THRESHOLD = 0.8;
|
|
15281
|
+
var MIN_TOKENS_FOR_CLEAR = 50000;
|
|
15282
|
+
var CLEAR_COOLDOWN_MS = 60000;
|
|
15283
|
+
function getContextLimit2(modelID) {
|
|
15284
|
+
const modelLower = modelID.toLowerCase();
|
|
15285
|
+
for (const [pattern, limit] of Object.entries(MODEL_CONTEXT_LIMITS2)) {
|
|
15286
|
+
if (modelLower.includes(pattern)) {
|
|
15287
|
+
return limit;
|
|
15288
|
+
}
|
|
15289
|
+
}
|
|
15290
|
+
return DEFAULT_CONTEXT_LIMIT3;
|
|
15291
|
+
}
|
|
15292
|
+
function createAutoClearLedgerHook(ctx) {
|
|
15293
|
+
const state = {
|
|
15294
|
+
lastClearTime: new Map,
|
|
15295
|
+
clearInProgress: new Set
|
|
15296
|
+
};
|
|
15297
|
+
async function checkAndClear(sessionID, _providerID, modelID) {
|
|
15298
|
+
if (state.clearInProgress.has(sessionID))
|
|
15299
|
+
return;
|
|
15300
|
+
const lastTime = state.lastClearTime.get(sessionID) || 0;
|
|
15301
|
+
if (Date.now() - lastTime < CLEAR_COOLDOWN_MS)
|
|
15302
|
+
return;
|
|
15303
|
+
try {
|
|
15304
|
+
const resp = await ctx.client.session.messages({
|
|
15305
|
+
path: { id: sessionID },
|
|
15306
|
+
query: { directory: ctx.directory }
|
|
15307
|
+
});
|
|
15308
|
+
const messages = resp.data;
|
|
15309
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
15310
|
+
return;
|
|
15311
|
+
const lastAssistant = [...messages].reverse().find((m) => {
|
|
15312
|
+
const msg = m;
|
|
15313
|
+
const info2 = msg.info;
|
|
15314
|
+
return info2?.role === "assistant";
|
|
15315
|
+
});
|
|
15316
|
+
if (!lastAssistant)
|
|
15317
|
+
return;
|
|
15318
|
+
const info = lastAssistant.info;
|
|
15319
|
+
const usage = info?.usage;
|
|
15320
|
+
const inputTokens = usage?.inputTokens || 0;
|
|
15321
|
+
const cacheRead = usage?.cacheReadInputTokens || 0;
|
|
15322
|
+
const totalUsed = inputTokens + cacheRead;
|
|
15323
|
+
if (totalUsed < MIN_TOKENS_FOR_CLEAR)
|
|
15324
|
+
return;
|
|
15325
|
+
const model = modelID || info?.modelID || "";
|
|
15326
|
+
const contextLimit = getContextLimit2(model);
|
|
15327
|
+
const usageRatio = totalUsed / contextLimit;
|
|
15328
|
+
if (usageRatio < DEFAULT_THRESHOLD)
|
|
15329
|
+
return;
|
|
15330
|
+
state.clearInProgress.add(sessionID);
|
|
15331
|
+
state.lastClearTime.set(sessionID, Date.now());
|
|
15332
|
+
await ctx.client.tui.showToast({
|
|
15333
|
+
body: {
|
|
15334
|
+
title: "Context Window",
|
|
15335
|
+
message: `${Math.round(usageRatio * 100)}% used - saving ledger and clearing...`,
|
|
15336
|
+
variant: "warning",
|
|
15337
|
+
duration: 3000
|
|
15338
|
+
}
|
|
15339
|
+
}).catch(() => {});
|
|
15340
|
+
const ledgerSessionResp = await ctx.client.session.create({
|
|
15341
|
+
body: {},
|
|
15342
|
+
query: { directory: ctx.directory }
|
|
15343
|
+
});
|
|
15344
|
+
const ledgerSessionID = ledgerSessionResp.data?.id;
|
|
15345
|
+
if (ledgerSessionID) {
|
|
15346
|
+
await ctx.client.session.prompt({
|
|
15347
|
+
path: { id: ledgerSessionID },
|
|
15348
|
+
body: {
|
|
15349
|
+
parts: [
|
|
15350
|
+
{ type: "text", text: "Update the continuity ledger with current session state before context clear." }
|
|
15351
|
+
],
|
|
15352
|
+
agent: "ledger-creator"
|
|
15353
|
+
},
|
|
15354
|
+
query: { directory: ctx.directory }
|
|
15355
|
+
});
|
|
15356
|
+
let attempts = 0;
|
|
15357
|
+
while (attempts < 30) {
|
|
15358
|
+
await new Promise((resolve2) => setTimeout(resolve2, 2000));
|
|
15359
|
+
const statusResp = await ctx.client.session.get({
|
|
15360
|
+
path: { id: ledgerSessionID },
|
|
15361
|
+
query: { directory: ctx.directory }
|
|
15362
|
+
});
|
|
15363
|
+
if (statusResp.data?.status === "idle") {
|
|
15364
|
+
break;
|
|
15365
|
+
}
|
|
15366
|
+
attempts++;
|
|
15367
|
+
}
|
|
15368
|
+
}
|
|
15369
|
+
const handoffSessionResp = await ctx.client.session.create({
|
|
15370
|
+
body: {},
|
|
15371
|
+
query: { directory: ctx.directory }
|
|
15372
|
+
});
|
|
15373
|
+
const handoffSessionID = handoffSessionResp.data?.id;
|
|
15374
|
+
if (handoffSessionID) {
|
|
15375
|
+
await ctx.client.session.prompt({
|
|
15376
|
+
path: { id: handoffSessionID },
|
|
15377
|
+
body: {
|
|
15378
|
+
parts: [
|
|
15379
|
+
{
|
|
15380
|
+
type: "text",
|
|
15381
|
+
text: "Create a handoff document. Read the current ledger at thoughts/ledgers/ for context."
|
|
15382
|
+
}
|
|
15383
|
+
],
|
|
15384
|
+
agent: "handoff-creator"
|
|
15385
|
+
},
|
|
15386
|
+
query: { directory: ctx.directory }
|
|
15387
|
+
});
|
|
15388
|
+
let attempts = 0;
|
|
15389
|
+
while (attempts < 30) {
|
|
15390
|
+
await new Promise((resolve2) => setTimeout(resolve2, 2000));
|
|
15391
|
+
const statusResp = await ctx.client.session.get({
|
|
15392
|
+
path: { id: handoffSessionID },
|
|
15393
|
+
query: { directory: ctx.directory }
|
|
15394
|
+
});
|
|
15395
|
+
if (statusResp.data?.status === "idle") {
|
|
15396
|
+
break;
|
|
15397
|
+
}
|
|
15398
|
+
attempts++;
|
|
15399
|
+
}
|
|
15400
|
+
}
|
|
15401
|
+
const firstMessage = messages[0];
|
|
15402
|
+
const firstMessageID = firstMessage?.info?.id;
|
|
15403
|
+
if (!firstMessageID) {
|
|
15404
|
+
throw new Error("Could not find first message ID for revert");
|
|
15405
|
+
}
|
|
15406
|
+
await ctx.client.session.revert({
|
|
15407
|
+
path: { id: sessionID },
|
|
15408
|
+
body: { messageID: firstMessageID },
|
|
15409
|
+
query: { directory: ctx.directory }
|
|
15410
|
+
});
|
|
15411
|
+
const ledger = await findCurrentLedger(ctx.directory);
|
|
15412
|
+
if (ledger) {
|
|
15413
|
+
const injection = formatLedgerInjection(ledger);
|
|
15414
|
+
await ctx.client.session.prompt({
|
|
15415
|
+
path: { id: sessionID },
|
|
15416
|
+
body: {
|
|
15417
|
+
parts: [{ type: "text", text: injection }],
|
|
15418
|
+
noReply: true
|
|
15419
|
+
},
|
|
15420
|
+
query: { directory: ctx.directory }
|
|
15421
|
+
});
|
|
15422
|
+
}
|
|
15423
|
+
await ctx.client.tui.showToast({
|
|
15424
|
+
body: {
|
|
15425
|
+
title: "Context Cleared",
|
|
15426
|
+
message: "Ledger + handoff saved. Session ready to continue.",
|
|
15427
|
+
variant: "success",
|
|
15428
|
+
duration: 5000
|
|
15429
|
+
}
|
|
15430
|
+
}).catch(() => {});
|
|
15431
|
+
} catch (e) {
|
|
15432
|
+
console.error("[auto-clear-ledger] Error:", e);
|
|
15433
|
+
await ctx.client.tui.showToast({
|
|
15434
|
+
body: {
|
|
15435
|
+
title: "Clear Failed",
|
|
15436
|
+
message: "Could not complete context clear. Continuing normally.",
|
|
15437
|
+
variant: "error",
|
|
15438
|
+
duration: 5000
|
|
15439
|
+
}
|
|
15440
|
+
}).catch(() => {});
|
|
15441
|
+
} finally {
|
|
15442
|
+
state.clearInProgress.delete(sessionID);
|
|
15443
|
+
}
|
|
15444
|
+
}
|
|
15445
|
+
return {
|
|
15446
|
+
event: async ({ event }) => {
|
|
15447
|
+
const props = event.properties;
|
|
15448
|
+
if (event.type === "session.deleted") {
|
|
15449
|
+
const sessionInfo = props?.info;
|
|
15450
|
+
if (sessionInfo?.id) {
|
|
15451
|
+
state.lastClearTime.delete(sessionInfo.id);
|
|
15452
|
+
state.clearInProgress.delete(sessionInfo.id);
|
|
15453
|
+
}
|
|
15454
|
+
return;
|
|
15455
|
+
}
|
|
15456
|
+
if (event.type === "message.updated") {
|
|
15457
|
+
const info = props?.info;
|
|
15458
|
+
const sessionID = info?.sessionID;
|
|
15459
|
+
if (sessionID && info?.role === "assistant") {
|
|
15460
|
+
const providerID = info.providerID;
|
|
15461
|
+
const modelID = info.modelID;
|
|
15462
|
+
await checkAndClear(sessionID, providerID, modelID);
|
|
15463
|
+
}
|
|
15464
|
+
}
|
|
15465
|
+
if (event.type === "session.idle") {
|
|
15466
|
+
const sessionID = props?.sessionID;
|
|
15467
|
+
if (sessionID) {
|
|
15468
|
+
await checkAndClear(sessionID);
|
|
15469
|
+
}
|
|
15470
|
+
}
|
|
15471
|
+
}
|
|
15472
|
+
};
|
|
15473
|
+
}
|
|
15474
|
+
|
|
15475
|
+
// src/hooks/artifact-auto-index.ts
|
|
15476
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
15477
|
+
var LEDGER_PATH_PATTERN = /thoughts\/ledgers\/CONTINUITY_(.+)\.md$/;
|
|
15478
|
+
var HANDOFF_PATH_PATTERN = /thoughts\/shared\/handoffs\/(.+)\.md$/;
|
|
15479
|
+
var PLAN_PATH_PATTERN = /thoughts\/shared\/plans\/(.+)\.md$/;
|
|
15480
|
+
function parseLedger(content, filePath, sessionName) {
|
|
15481
|
+
const goalMatch = content.match(/## Goal\n([^\n]+)/);
|
|
15482
|
+
const stateMatch = content.match(/- Now: ([^\n]+)/);
|
|
15483
|
+
const decisionsMatch = content.match(/## Key Decisions\n([\s\S]*?)(?=\n## |$)/);
|
|
15484
|
+
return {
|
|
15485
|
+
id: `ledger-${sessionName}`,
|
|
15486
|
+
sessionName,
|
|
15487
|
+
filePath,
|
|
15488
|
+
goal: goalMatch?.[1] || "",
|
|
15489
|
+
stateNow: stateMatch?.[1] || "",
|
|
15490
|
+
keyDecisions: decisionsMatch?.[1]?.trim() || ""
|
|
15491
|
+
};
|
|
15492
|
+
}
|
|
15493
|
+
function parseHandoff(content, filePath, fileName) {
|
|
15494
|
+
const sessionMatch = content.match(/^session:\s*(.+)$/m);
|
|
15495
|
+
const sessionName = sessionMatch?.[1] || fileName;
|
|
15496
|
+
const taskMatch = content.match(/\*\*Working on:\*\*\s*([^\n]+)/);
|
|
15497
|
+
const taskSummary = taskMatch?.[1] || "";
|
|
15498
|
+
const learningsMatch = content.match(/## Learnings\n\n([\s\S]*?)(?=\n## |$)/);
|
|
15499
|
+
const learnings = learningsMatch?.[1]?.trim() || "";
|
|
15500
|
+
const workedMatch = content.match(/## What Worked\n\n([\s\S]*?)(?=\n## |$)/);
|
|
15501
|
+
const whatWorked = workedMatch?.[1]?.trim() || learnings;
|
|
15502
|
+
const failedMatch = content.match(/## What Failed\n\n([\s\S]*?)(?=\n## |$)/);
|
|
15503
|
+
const whatFailed = failedMatch?.[1]?.trim() || "";
|
|
15504
|
+
return {
|
|
15505
|
+
id: `handoff-${fileName}`,
|
|
15506
|
+
sessionName,
|
|
15507
|
+
filePath,
|
|
15508
|
+
taskSummary,
|
|
15509
|
+
whatWorked,
|
|
15510
|
+
whatFailed,
|
|
15511
|
+
learnings,
|
|
15512
|
+
outcome: "UNKNOWN"
|
|
15513
|
+
};
|
|
15514
|
+
}
|
|
15515
|
+
function parsePlan(content, filePath, fileName) {
|
|
15516
|
+
const titleMatch = content.match(/^# (.+)$/m);
|
|
15517
|
+
const title = titleMatch?.[1] || fileName;
|
|
15518
|
+
const overviewMatch = content.match(/## Overview\n\n([\s\S]*?)(?=\n## |$)/);
|
|
15519
|
+
const overview = overviewMatch?.[1]?.trim() || "";
|
|
15520
|
+
const approachMatch = content.match(/## Approach\n\n([\s\S]*?)(?=\n## |$)/);
|
|
15521
|
+
const approach = approachMatch?.[1]?.trim() || "";
|
|
15522
|
+
return {
|
|
15523
|
+
id: `plan-${fileName}`,
|
|
15524
|
+
title,
|
|
15525
|
+
filePath,
|
|
15526
|
+
overview,
|
|
15527
|
+
approach
|
|
15528
|
+
};
|
|
15529
|
+
}
|
|
15530
|
+
function createArtifactAutoIndexHook(_ctx) {
|
|
15531
|
+
return {
|
|
15532
|
+
"tool.execute.after": async (input, _output) => {
|
|
15533
|
+
if (input.tool !== "write")
|
|
15534
|
+
return;
|
|
15535
|
+
const filePath = input.args?.filePath;
|
|
15536
|
+
if (!filePath)
|
|
15537
|
+
return;
|
|
15538
|
+
try {
|
|
15539
|
+
const ledgerMatch = filePath.match(LEDGER_PATH_PATTERN);
|
|
15540
|
+
if (ledgerMatch) {
|
|
15541
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
15542
|
+
const index = await getArtifactIndex();
|
|
15543
|
+
const record2 = parseLedger(content, filePath, ledgerMatch[1]);
|
|
15544
|
+
await index.indexLedger(record2);
|
|
15545
|
+
console.log(`[artifact-auto-index] Indexed ledger: ${filePath}`);
|
|
15546
|
+
return;
|
|
15547
|
+
}
|
|
15548
|
+
const handoffMatch = filePath.match(HANDOFF_PATH_PATTERN);
|
|
15549
|
+
if (handoffMatch) {
|
|
15550
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
15551
|
+
const index = await getArtifactIndex();
|
|
15552
|
+
const record2 = parseHandoff(content, filePath, handoffMatch[1]);
|
|
15553
|
+
await index.indexHandoff(record2);
|
|
15554
|
+
console.log(`[artifact-auto-index] Indexed handoff: ${filePath}`);
|
|
15555
|
+
return;
|
|
15556
|
+
}
|
|
15557
|
+
const planMatch = filePath.match(PLAN_PATH_PATTERN);
|
|
15558
|
+
if (planMatch) {
|
|
15559
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
15560
|
+
const index = await getArtifactIndex();
|
|
15561
|
+
const record2 = parsePlan(content, filePath, planMatch[1]);
|
|
15562
|
+
await index.indexPlan(record2);
|
|
15563
|
+
console.log(`[artifact-auto-index] Indexed plan: ${filePath}`);
|
|
15564
|
+
return;
|
|
15565
|
+
}
|
|
15566
|
+
} catch (e) {
|
|
15567
|
+
console.error(`[artifact-auto-index] Error indexing ${filePath}:`, e);
|
|
15568
|
+
}
|
|
15569
|
+
}
|
|
15570
|
+
};
|
|
15571
|
+
}
|
|
15572
|
+
|
|
14929
15573
|
// src/tools/background-task/manager.ts
|
|
14930
15574
|
var POLL_INTERVAL_MS = 2000;
|
|
14931
15575
|
function generateTaskId() {
|
|
@@ -15096,7 +15740,7 @@ ${task.error}
|
|
|
15096
15740
|
`;
|
|
15097
15741
|
}
|
|
15098
15742
|
if (task.progress?.lastMessage) {
|
|
15099
|
-
const preview = task.progress.lastMessage.length > 200 ? task.progress.lastMessage.slice(0, 200)
|
|
15743
|
+
const preview = task.progress.lastMessage.length > 200 ? `${task.progress.lastMessage.slice(0, 200)}...` : task.progress.lastMessage;
|
|
15100
15744
|
output += `
|
|
15101
15745
|
### Last Message Preview
|
|
15102
15746
|
${preview}
|
|
@@ -15336,6 +15980,18 @@ var MCP_SERVERS = {
|
|
|
15336
15980
|
command: ["npx", "-y", "@upstash/context7-mcp@latest"]
|
|
15337
15981
|
}
|
|
15338
15982
|
};
|
|
15983
|
+
if (process.env.PERPLEXITY_API_KEY) {
|
|
15984
|
+
MCP_SERVERS.perplexity = {
|
|
15985
|
+
type: "local",
|
|
15986
|
+
command: ["npx", "-y", "@anthropic/mcp-perplexity"]
|
|
15987
|
+
};
|
|
15988
|
+
}
|
|
15989
|
+
if (process.env.FIRECRAWL_API_KEY) {
|
|
15990
|
+
MCP_SERVERS.firecrawl = {
|
|
15991
|
+
type: "local",
|
|
15992
|
+
command: ["npx", "-y", "firecrawl-mcp"]
|
|
15993
|
+
};
|
|
15994
|
+
}
|
|
15339
15995
|
var OpenCodeConfigPlugin = async (ctx) => {
|
|
15340
15996
|
const astGrepStatus = await checkAstGrepAvailable();
|
|
15341
15997
|
if (!astGrepStatus.available) {
|
|
@@ -15344,11 +16000,13 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15344
16000
|
const thinkModeState = new Map;
|
|
15345
16001
|
const autoCompactHook = createAutoCompactHook(ctx);
|
|
15346
16002
|
const contextInjectorHook = createContextInjectorHook(ctx);
|
|
15347
|
-
const
|
|
16003
|
+
const autoClearLedgerHook = createAutoClearLedgerHook(ctx);
|
|
16004
|
+
const ledgerLoaderHook = createLedgerLoaderHook(ctx);
|
|
15348
16005
|
const sessionRecoveryHook = createSessionRecoveryHook(ctx);
|
|
15349
16006
|
const tokenAwareTruncationHook = createTokenAwareTruncationHook(ctx);
|
|
15350
16007
|
const contextWindowMonitorHook = createContextWindowMonitorHook(ctx);
|
|
15351
16008
|
const commentCheckerHook = createCommentCheckerHook(ctx);
|
|
16009
|
+
const artifactAutoIndexHook = createArtifactAutoIndexHook(ctx);
|
|
15352
16010
|
const backgroundTaskManager = new BackgroundTaskManager(ctx);
|
|
15353
16011
|
const backgroundTaskTools = createBackgroundTaskTools(backgroundTaskManager);
|
|
15354
16012
|
return {
|
|
@@ -15356,6 +16014,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15356
16014
|
ast_grep_search,
|
|
15357
16015
|
ast_grep_replace,
|
|
15358
16016
|
look_at,
|
|
16017
|
+
artifact_search,
|
|
15359
16018
|
...backgroundTaskTools
|
|
15360
16019
|
},
|
|
15361
16020
|
config: async (config2) => {
|
|
@@ -15384,6 +16043,16 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15384
16043
|
description: "Initialize project with ARCHITECTURE.md and CODE_STYLE.md",
|
|
15385
16044
|
agent: "project-initializer",
|
|
15386
16045
|
template: `Initialize this project. $ARGUMENTS`
|
|
16046
|
+
},
|
|
16047
|
+
ledger: {
|
|
16048
|
+
description: "Create or update continuity ledger for session state",
|
|
16049
|
+
agent: "ledger-creator",
|
|
16050
|
+
template: `Update the continuity ledger. $ARGUMENTS`
|
|
16051
|
+
},
|
|
16052
|
+
search: {
|
|
16053
|
+
description: "Search past handoffs, plans, and ledgers",
|
|
16054
|
+
agent: "artifact-searcher",
|
|
16055
|
+
template: `Search for: $ARGUMENTS`
|
|
15387
16056
|
}
|
|
15388
16057
|
};
|
|
15389
16058
|
},
|
|
@@ -15392,6 +16061,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15392
16061
|
thinkModeState.set(input.sessionID, detectThinkKeyword(text));
|
|
15393
16062
|
},
|
|
15394
16063
|
"chat.params": async (input, output) => {
|
|
16064
|
+
await ledgerLoaderHook["chat.params"](input, output);
|
|
15395
16065
|
await contextInjectorHook["chat.params"](input, output);
|
|
15396
16066
|
await contextWindowMonitorHook["chat.params"](input, output);
|
|
15397
16067
|
if (thinkModeState.get(input.sessionID)) {
|
|
@@ -15408,6 +16078,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15408
16078
|
await tokenAwareTruncationHook["tool.execute.after"]({ name: input.tool, sessionID: input.sessionID }, output);
|
|
15409
16079
|
await commentCheckerHook["tool.execute.after"]({ tool: input.tool, args: input.args }, output);
|
|
15410
16080
|
await contextInjectorHook["tool.execute.after"]({ tool: input.tool, args: input.args }, output);
|
|
16081
|
+
await artifactAutoIndexHook["tool.execute.after"]({ tool: input.tool, args: input.args }, output);
|
|
15411
16082
|
},
|
|
15412
16083
|
event: async ({ event }) => {
|
|
15413
16084
|
if (event.type === "session.deleted") {
|
|
@@ -15417,7 +16088,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15417
16088
|
}
|
|
15418
16089
|
}
|
|
15419
16090
|
await autoCompactHook.event({ event });
|
|
15420
|
-
await
|
|
16091
|
+
await autoClearLedgerHook.event({ event });
|
|
15421
16092
|
await sessionRecoveryHook.event({ event });
|
|
15422
16093
|
await tokenAwareTruncationHook.event({ event });
|
|
15423
16094
|
await contextWindowMonitorHook.event({ event });
|