micode 0.3.1 → 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 +857 -193
- 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
|
@@ -869,6 +869,8 @@ Create handoff document to transfer context to future session.
|
|
|
869
869
|
</when-to-use>
|
|
870
870
|
|
|
871
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>
|
|
872
874
|
<rule>Capture ALL in-progress work</rule>
|
|
873
875
|
<rule>Include exact file:line references for changes</rule>
|
|
874
876
|
<rule>Document learnings and gotchas</rule>
|
|
@@ -878,6 +880,8 @@ Create handoff document to transfer context to future session.
|
|
|
878
880
|
</rules>
|
|
879
881
|
|
|
880
882
|
<process>
|
|
883
|
+
<step>Check for ledger at thoughts/ledgers/CONTINUITY_*.md</step>
|
|
884
|
+
<step>If ledger exists, extract session name and state</step>
|
|
881
885
|
<step>Review what was worked on</step>
|
|
882
886
|
<step>Check git status for uncommitted changes</step>
|
|
883
887
|
<step>Gather learnings and decisions made</step>
|
|
@@ -886,13 +890,17 @@ Create handoff document to transfer context to future session.
|
|
|
886
890
|
<step>Commit handoff document</step>
|
|
887
891
|
</process>
|
|
888
892
|
|
|
889
|
-
<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>
|
|
890
897
|
|
|
891
898
|
<document-format>
|
|
892
899
|
<frontmatter>
|
|
893
900
|
date: [ISO datetime]
|
|
894
901
|
branch: [branch name]
|
|
895
902
|
commit: [hash]
|
|
903
|
+
session: [session name from ledger, if available]
|
|
896
904
|
</frontmatter>
|
|
897
905
|
<sections>
|
|
898
906
|
<section name="Tasks">Table with Task | Status (completed/in-progress/blocked)</section>
|
|
@@ -1316,6 +1324,116 @@ var projectInitializerAgent = {
|
|
|
1316
1324
|
prompt: PROMPT2
|
|
1317
1325
|
};
|
|
1318
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
|
+
|
|
1319
1437
|
// src/agents/index.ts
|
|
1320
1438
|
var agents = {
|
|
1321
1439
|
[PRIMARY_AGENT_NAME]: primaryAgent,
|
|
@@ -1329,7 +1447,9 @@ var agents = {
|
|
|
1329
1447
|
executor: executorAgent,
|
|
1330
1448
|
"handoff-creator": handoffCreatorAgent,
|
|
1331
1449
|
"handoff-resumer": handoffResumerAgent,
|
|
1332
|
-
"project-initializer": projectInitializerAgent
|
|
1450
|
+
"project-initializer": projectInitializerAgent,
|
|
1451
|
+
"ledger-creator": ledgerCreatorAgent,
|
|
1452
|
+
"artifact-searcher": artifactSearcherAgent
|
|
1333
1453
|
};
|
|
1334
1454
|
|
|
1335
1455
|
// src/tools/ast-grep/index.ts
|
|
@@ -13744,7 +13864,7 @@ function formatMatches(matches, isDryRun = false) {
|
|
|
13744
13864
|
const shown = matches.slice(0, MAX);
|
|
13745
13865
|
const lines = shown.map((m) => {
|
|
13746
13866
|
const loc = `${m.file}:${m.range.start.line}:${m.range.start.column}`;
|
|
13747
|
-
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;
|
|
13748
13868
|
if (isDryRun && m.replacement) {
|
|
13749
13869
|
return `${loc}
|
|
13750
13870
|
- ${text}
|
|
@@ -13788,16 +13908,7 @@ var ast_grep_replace = tool({
|
|
|
13788
13908
|
apply: tool.schema.boolean().optional().describe("Apply changes (default: false, dry-run)")
|
|
13789
13909
|
},
|
|
13790
13910
|
execute: async (args) => {
|
|
13791
|
-
const sgArgs = [
|
|
13792
|
-
"run",
|
|
13793
|
-
"-p",
|
|
13794
|
-
args.pattern,
|
|
13795
|
-
"-r",
|
|
13796
|
-
args.rewrite,
|
|
13797
|
-
"--lang",
|
|
13798
|
-
args.lang,
|
|
13799
|
-
"--json=compact"
|
|
13800
|
-
];
|
|
13911
|
+
const sgArgs = ["run", "-p", args.pattern, "-r", args.rewrite, "--lang", args.lang, "--json=compact"];
|
|
13801
13912
|
if (args.apply) {
|
|
13802
13913
|
sgArgs.push("--update-all");
|
|
13803
13914
|
}
|
|
@@ -13812,7 +13923,7 @@ var ast_grep_replace = tool({
|
|
|
13812
13923
|
const isDryRun = !args.apply;
|
|
13813
13924
|
const output = formatMatches(result.matches, isDryRun);
|
|
13814
13925
|
if (isDryRun && result.matches.length > 0) {
|
|
13815
|
-
return output
|
|
13926
|
+
return `${output}
|
|
13816
13927
|
|
|
13817
13928
|
(Dry run - use apply=true to apply changes)`;
|
|
13818
13929
|
}
|
|
@@ -13829,7 +13940,20 @@ import { readFileSync, statSync } from "fs";
|
|
|
13829
13940
|
import { extname, basename } from "path";
|
|
13830
13941
|
var LARGE_FILE_THRESHOLD = 100 * 1024;
|
|
13831
13942
|
var MAX_LINES_WITHOUT_EXTRACT = 200;
|
|
13832
|
-
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
|
+
];
|
|
13833
13957
|
function extractStructure(content, ext) {
|
|
13834
13958
|
const lines = content.split(`
|
|
13835
13959
|
`);
|
|
@@ -13861,7 +13985,7 @@ function extractTypeScriptStructure(lines) {
|
|
|
13861
13985
|
const line = lines[i];
|
|
13862
13986
|
const trimmed = line.trim();
|
|
13863
13987
|
if (trimmed.startsWith("export ") || trimmed.startsWith("class ") || trimmed.startsWith("interface ") || trimmed.startsWith("type ") || trimmed.startsWith("function ") || trimmed.startsWith("const ") || trimmed.startsWith("async function ")) {
|
|
13864
|
-
const signature = trimmed.length > 80 ? trimmed.slice(0, 80)
|
|
13988
|
+
const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
|
|
13865
13989
|
output.push(`Line ${i + 1}: ${signature}`);
|
|
13866
13990
|
}
|
|
13867
13991
|
}
|
|
@@ -13875,7 +13999,7 @@ function extractPythonStructure(lines) {
|
|
|
13875
13999
|
const line = lines[i];
|
|
13876
14000
|
const trimmed = line.trim();
|
|
13877
14001
|
if (trimmed.startsWith("class ") || trimmed.startsWith("def ") || trimmed.startsWith("async def ") || trimmed.startsWith("@")) {
|
|
13878
|
-
const signature = trimmed.length > 80 ? trimmed.slice(0, 80)
|
|
14002
|
+
const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
|
|
13879
14003
|
output.push(`Line ${i + 1}: ${signature}`);
|
|
13880
14004
|
}
|
|
13881
14005
|
}
|
|
@@ -13889,7 +14013,7 @@ function extractGoStructure(lines) {
|
|
|
13889
14013
|
const line = lines[i];
|
|
13890
14014
|
const trimmed = line.trim();
|
|
13891
14015
|
if (trimmed.startsWith("type ") || trimmed.startsWith("func ") || trimmed.startsWith("package ")) {
|
|
13892
|
-
const signature = trimmed.length > 80 ? trimmed.slice(0, 80)
|
|
14016
|
+
const signature = trimmed.length > 80 ? `${trimmed.slice(0, 80)}...` : trimmed;
|
|
13893
14017
|
output.push(`Line ${i + 1}: ${signature}`);
|
|
13894
14018
|
}
|
|
13895
14019
|
}
|
|
@@ -13988,6 +14112,302 @@ ${content}`;
|
|
|
13988
14112
|
}
|
|
13989
14113
|
});
|
|
13990
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
|
+
|
|
13991
14411
|
// src/hooks/auto-compact.ts
|
|
13992
14412
|
function parseTokenLimitError(error45) {
|
|
13993
14413
|
if (!error45)
|
|
@@ -14083,9 +14503,9 @@ function createAutoCompactHook(ctx) {
|
|
|
14083
14503
|
}
|
|
14084
14504
|
}).catch(() => {});
|
|
14085
14505
|
}
|
|
14086
|
-
} catch (
|
|
14506
|
+
} catch (_e) {
|
|
14087
14507
|
state.retryCount.set(sessionID, retries + 1);
|
|
14088
|
-
const delay = Math.min(1000 *
|
|
14508
|
+
const delay = Math.min(1000 * 2 ** retries, 1e4);
|
|
14089
14509
|
setTimeout(() => {
|
|
14090
14510
|
state.inProgress.delete(sessionID);
|
|
14091
14511
|
attemptRecovery(sessionID, providerID, modelID);
|
|
@@ -14167,14 +14587,8 @@ function createAutoCompactHook(ctx) {
|
|
|
14167
14587
|
|
|
14168
14588
|
// src/hooks/context-injector.ts
|
|
14169
14589
|
import { readFile } from "fs/promises";
|
|
14170
|
-
import { join, dirname, resolve } from "path";
|
|
14171
|
-
var ROOT_CONTEXT_FILES = [
|
|
14172
|
-
"ARCHITECTURE.md",
|
|
14173
|
-
"CODE_STYLE.md",
|
|
14174
|
-
"AGENTS.md",
|
|
14175
|
-
"CLAUDE.md",
|
|
14176
|
-
"README.md"
|
|
14177
|
-
];
|
|
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"];
|
|
14178
14592
|
var DIRECTORY_CONTEXT_FILES = ["AGENTS.md", "README.md"];
|
|
14179
14593
|
var FILE_ACCESS_TOOLS = ["Read", "read", "Edit", "edit"];
|
|
14180
14594
|
var CACHE_TTL = 30000;
|
|
@@ -14193,7 +14607,7 @@ function createContextInjectorHook(ctx) {
|
|
|
14193
14607
|
cache.lastRootCheck = now;
|
|
14194
14608
|
for (const filename of ROOT_CONTEXT_FILES) {
|
|
14195
14609
|
try {
|
|
14196
|
-
const filepath =
|
|
14610
|
+
const filepath = join2(ctx.directory, filename);
|
|
14197
14611
|
const content = await readFile(filepath, "utf-8");
|
|
14198
14612
|
if (content.trim()) {
|
|
14199
14613
|
cache.rootContent.set(filename, content);
|
|
@@ -14205,15 +14619,15 @@ function createContextInjectorHook(ctx) {
|
|
|
14205
14619
|
async function walkUpForContextFiles(filePath) {
|
|
14206
14620
|
const absPath = resolve(filePath);
|
|
14207
14621
|
const projectRoot = resolve(ctx.directory);
|
|
14208
|
-
const cacheKey =
|
|
14622
|
+
const cacheKey = dirname2(absPath);
|
|
14209
14623
|
if (cache.directoryContent.has(cacheKey)) {
|
|
14210
14624
|
return cache.directoryContent.get(cacheKey);
|
|
14211
14625
|
}
|
|
14212
14626
|
const collected = new Map;
|
|
14213
|
-
let currentDir =
|
|
14627
|
+
let currentDir = dirname2(absPath);
|
|
14214
14628
|
while (currentDir.startsWith(projectRoot) || currentDir === projectRoot) {
|
|
14215
14629
|
for (const filename of DIRECTORY_CONTEXT_FILES) {
|
|
14216
|
-
const contextPath =
|
|
14630
|
+
const contextPath = join2(currentDir, filename);
|
|
14217
14631
|
const relPath = currentDir.replace(projectRoot, "").replace(/^\//, "") || ".";
|
|
14218
14632
|
const key = `${relPath}/${filename}`;
|
|
14219
14633
|
if (!collected.has(key)) {
|
|
@@ -14227,7 +14641,7 @@ function createContextInjectorHook(ctx) {
|
|
|
14227
14641
|
}
|
|
14228
14642
|
if (currentDir === projectRoot)
|
|
14229
14643
|
break;
|
|
14230
|
-
const parent =
|
|
14644
|
+
const parent = dirname2(currentDir);
|
|
14231
14645
|
if (parent === currentDir)
|
|
14232
14646
|
break;
|
|
14233
14647
|
currentDir = parent;
|
|
@@ -14288,144 +14702,6 @@ ${blocks.join(`
|
|
|
14288
14702
|
};
|
|
14289
14703
|
}
|
|
14290
14704
|
|
|
14291
|
-
// src/hooks/preemptive-compaction.ts
|
|
14292
|
-
var MODEL_CONTEXT_LIMITS = {
|
|
14293
|
-
"claude-opus": 200000,
|
|
14294
|
-
"claude-sonnet": 200000,
|
|
14295
|
-
"claude-haiku": 200000,
|
|
14296
|
-
"claude-3": 200000,
|
|
14297
|
-
"claude-4": 200000,
|
|
14298
|
-
"gpt-4o": 128000,
|
|
14299
|
-
"gpt-4-turbo": 128000,
|
|
14300
|
-
"gpt-4": 128000,
|
|
14301
|
-
"gpt-5": 200000,
|
|
14302
|
-
o1: 200000,
|
|
14303
|
-
o3: 200000,
|
|
14304
|
-
gemini: 1e6
|
|
14305
|
-
};
|
|
14306
|
-
var DEFAULT_CONTEXT_LIMIT = 200000;
|
|
14307
|
-
var DEFAULT_THRESHOLD = 0.8;
|
|
14308
|
-
var MIN_TOKENS_FOR_COMPACTION = 50000;
|
|
14309
|
-
var COMPACTION_COOLDOWN_MS = 60000;
|
|
14310
|
-
function getContextLimit(modelID) {
|
|
14311
|
-
const modelLower = modelID.toLowerCase();
|
|
14312
|
-
for (const [pattern, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
|
|
14313
|
-
if (modelLower.includes(pattern)) {
|
|
14314
|
-
return limit;
|
|
14315
|
-
}
|
|
14316
|
-
}
|
|
14317
|
-
return DEFAULT_CONTEXT_LIMIT;
|
|
14318
|
-
}
|
|
14319
|
-
function createPreemptiveCompactionHook(ctx) {
|
|
14320
|
-
const state = {
|
|
14321
|
-
lastCompactionTime: new Map,
|
|
14322
|
-
compactionInProgress: new Set
|
|
14323
|
-
};
|
|
14324
|
-
async function checkAndCompact(sessionID, providerID, modelID) {
|
|
14325
|
-
if (state.compactionInProgress.has(sessionID))
|
|
14326
|
-
return;
|
|
14327
|
-
const lastTime = state.lastCompactionTime.get(sessionID) || 0;
|
|
14328
|
-
if (Date.now() - lastTime < COMPACTION_COOLDOWN_MS)
|
|
14329
|
-
return;
|
|
14330
|
-
try {
|
|
14331
|
-
const resp = await ctx.client.session.messages({
|
|
14332
|
-
path: { id: sessionID },
|
|
14333
|
-
query: { directory: ctx.directory }
|
|
14334
|
-
});
|
|
14335
|
-
const messages = resp.data;
|
|
14336
|
-
if (!Array.isArray(messages) || messages.length === 0)
|
|
14337
|
-
return;
|
|
14338
|
-
const lastAssistant = [...messages].reverse().find((m) => {
|
|
14339
|
-
const msg = m;
|
|
14340
|
-
const info2 = msg.info;
|
|
14341
|
-
return info2?.role === "assistant";
|
|
14342
|
-
});
|
|
14343
|
-
if (!lastAssistant)
|
|
14344
|
-
return;
|
|
14345
|
-
const info = lastAssistant.info;
|
|
14346
|
-
const usage = info?.usage;
|
|
14347
|
-
const inputTokens = usage?.inputTokens || 0;
|
|
14348
|
-
const cacheRead = usage?.cacheReadInputTokens || 0;
|
|
14349
|
-
const totalUsed = inputTokens + cacheRead;
|
|
14350
|
-
if (totalUsed < MIN_TOKENS_FOR_COMPACTION)
|
|
14351
|
-
return;
|
|
14352
|
-
const model = modelID || info?.modelID || "";
|
|
14353
|
-
const contextLimit = getContextLimit(model);
|
|
14354
|
-
const usageRatio = totalUsed / contextLimit;
|
|
14355
|
-
if (usageRatio < DEFAULT_THRESHOLD)
|
|
14356
|
-
return;
|
|
14357
|
-
const lastUserMsg = [...messages].reverse().find((m) => {
|
|
14358
|
-
const msg = m;
|
|
14359
|
-
const msgInfo = msg.info;
|
|
14360
|
-
return msgInfo?.role === "user";
|
|
14361
|
-
});
|
|
14362
|
-
if (lastUserMsg) {
|
|
14363
|
-
const parts = lastUserMsg.parts;
|
|
14364
|
-
const text = parts?.find((p) => p.type === "text")?.text || "";
|
|
14365
|
-
if (text.includes("summarized") || text.includes("compacted"))
|
|
14366
|
-
return;
|
|
14367
|
-
}
|
|
14368
|
-
state.compactionInProgress.add(sessionID);
|
|
14369
|
-
state.lastCompactionTime.set(sessionID, Date.now());
|
|
14370
|
-
await ctx.client.tui.showToast({
|
|
14371
|
-
body: {
|
|
14372
|
-
title: "Context Window",
|
|
14373
|
-
message: `${Math.round(usageRatio * 100)}% used - auto-compacting...`,
|
|
14374
|
-
variant: "warning",
|
|
14375
|
-
duration: 3000
|
|
14376
|
-
}
|
|
14377
|
-
}).catch(() => {});
|
|
14378
|
-
const provider = providerID || info?.providerID;
|
|
14379
|
-
const modelToUse = modelID || info?.modelID;
|
|
14380
|
-
if (provider && modelToUse) {
|
|
14381
|
-
await ctx.client.session.summarize({
|
|
14382
|
-
path: { id: sessionID },
|
|
14383
|
-
body: { providerID: provider, modelID: modelToUse },
|
|
14384
|
-
query: { directory: ctx.directory }
|
|
14385
|
-
});
|
|
14386
|
-
await ctx.client.tui.showToast({
|
|
14387
|
-
body: {
|
|
14388
|
-
title: "Compacted",
|
|
14389
|
-
message: "Session summarized successfully",
|
|
14390
|
-
variant: "success",
|
|
14391
|
-
duration: 3000
|
|
14392
|
-
}
|
|
14393
|
-
}).catch(() => {});
|
|
14394
|
-
}
|
|
14395
|
-
} catch (e) {} finally {
|
|
14396
|
-
state.compactionInProgress.delete(sessionID);
|
|
14397
|
-
}
|
|
14398
|
-
}
|
|
14399
|
-
return {
|
|
14400
|
-
event: async ({ event }) => {
|
|
14401
|
-
const props = event.properties;
|
|
14402
|
-
if (event.type === "session.deleted") {
|
|
14403
|
-
const sessionInfo = props?.info;
|
|
14404
|
-
if (sessionInfo?.id) {
|
|
14405
|
-
state.lastCompactionTime.delete(sessionInfo.id);
|
|
14406
|
-
state.compactionInProgress.delete(sessionInfo.id);
|
|
14407
|
-
}
|
|
14408
|
-
return;
|
|
14409
|
-
}
|
|
14410
|
-
if (event.type === "message.updated") {
|
|
14411
|
-
const info = props?.info;
|
|
14412
|
-
const sessionID = info?.sessionID;
|
|
14413
|
-
if (sessionID && info?.role === "assistant") {
|
|
14414
|
-
const providerID = info.providerID;
|
|
14415
|
-
const modelID = info.modelID;
|
|
14416
|
-
await checkAndCompact(sessionID, providerID, modelID);
|
|
14417
|
-
}
|
|
14418
|
-
}
|
|
14419
|
-
if (event.type === "session.idle") {
|
|
14420
|
-
const sessionID = props?.sessionID;
|
|
14421
|
-
if (sessionID) {
|
|
14422
|
-
await checkAndCompact(sessionID);
|
|
14423
|
-
}
|
|
14424
|
-
}
|
|
14425
|
-
}
|
|
14426
|
-
};
|
|
14427
|
-
}
|
|
14428
|
-
|
|
14429
14705
|
// src/hooks/session-recovery.ts
|
|
14430
14706
|
var RECOVERABLE_ERRORS = {
|
|
14431
14707
|
TOOL_RESULT_MISSING: "tool_result block(s) missing",
|
|
@@ -14608,15 +14884,9 @@ function createSessionRecoveryHook(ctx) {
|
|
|
14608
14884
|
}
|
|
14609
14885
|
|
|
14610
14886
|
// src/hooks/token-aware-truncation.ts
|
|
14611
|
-
var TRUNCATABLE_TOOLS = [
|
|
14612
|
-
"grep",
|
|
14613
|
-
"Grep",
|
|
14614
|
-
"glob",
|
|
14615
|
-
"Glob",
|
|
14616
|
-
"ast_grep_search"
|
|
14617
|
-
];
|
|
14887
|
+
var TRUNCATABLE_TOOLS = ["grep", "Grep", "glob", "Glob", "ast_grep_search"];
|
|
14618
14888
|
var CHARS_PER_TOKEN = 4;
|
|
14619
|
-
var
|
|
14889
|
+
var DEFAULT_CONTEXT_LIMIT = 200000;
|
|
14620
14890
|
var DEFAULT_MAX_OUTPUT_TOKENS = 50000;
|
|
14621
14891
|
var SAFETY_MARGIN = 0.5;
|
|
14622
14892
|
var PRESERVE_HEADER_LINES = 3;
|
|
@@ -14677,7 +14947,7 @@ function createTokenAwareTruncationHook(ctx) {
|
|
|
14677
14947
|
});
|
|
14678
14948
|
const messages = resp.data;
|
|
14679
14949
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
14680
|
-
return { used: 0, limit:
|
|
14950
|
+
return { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
|
|
14681
14951
|
}
|
|
14682
14952
|
const lastAssistant = [...messages].reverse().find((m) => {
|
|
14683
14953
|
const msg = m;
|
|
@@ -14685,19 +14955,19 @@ function createTokenAwareTruncationHook(ctx) {
|
|
|
14685
14955
|
return info2?.role === "assistant";
|
|
14686
14956
|
});
|
|
14687
14957
|
if (!lastAssistant) {
|
|
14688
|
-
return { used: 0, limit:
|
|
14958
|
+
return { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
|
|
14689
14959
|
}
|
|
14690
14960
|
const info = lastAssistant.info;
|
|
14691
14961
|
const usage = info?.usage;
|
|
14692
14962
|
const inputTokens = usage?.inputTokens || 0;
|
|
14693
14963
|
const cacheRead = usage?.cacheReadInputTokens || 0;
|
|
14694
14964
|
const used = inputTokens + cacheRead;
|
|
14695
|
-
const limit =
|
|
14965
|
+
const limit = DEFAULT_CONTEXT_LIMIT;
|
|
14696
14966
|
const result = { used, limit };
|
|
14697
14967
|
state.sessionTokenUsage.set(sessionID, result);
|
|
14698
14968
|
return result;
|
|
14699
14969
|
} catch {
|
|
14700
|
-
return state.sessionTokenUsage.get(sessionID) || { used: 0, limit:
|
|
14970
|
+
return state.sessionTokenUsage.get(sessionID) || { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
|
|
14701
14971
|
}
|
|
14702
14972
|
}
|
|
14703
14973
|
function calculateMaxOutputTokens(used, limit) {
|
|
@@ -14757,8 +15027,8 @@ function createTokenAwareTruncationHook(ctx) {
|
|
|
14757
15027
|
// src/hooks/context-window-monitor.ts
|
|
14758
15028
|
var WARNING_THRESHOLD = 0.7;
|
|
14759
15029
|
var CRITICAL_THRESHOLD = 0.85;
|
|
14760
|
-
var
|
|
14761
|
-
var
|
|
15030
|
+
var DEFAULT_CONTEXT_LIMIT2 = 200000;
|
|
15031
|
+
var MODEL_CONTEXT_LIMITS = {
|
|
14762
15032
|
"claude-opus": 200000,
|
|
14763
15033
|
"claude-sonnet": 200000,
|
|
14764
15034
|
"claude-haiku": 200000,
|
|
@@ -14768,14 +15038,14 @@ var MODEL_CONTEXT_LIMITS2 = {
|
|
|
14768
15038
|
o3: 200000,
|
|
14769
15039
|
gemini: 1e6
|
|
14770
15040
|
};
|
|
14771
|
-
function
|
|
15041
|
+
function getContextLimit(modelID) {
|
|
14772
15042
|
const modelLower = modelID.toLowerCase();
|
|
14773
|
-
for (const [pattern, limit] of Object.entries(
|
|
15043
|
+
for (const [pattern, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
|
|
14774
15044
|
if (modelLower.includes(pattern)) {
|
|
14775
15045
|
return limit;
|
|
14776
15046
|
}
|
|
14777
15047
|
}
|
|
14778
|
-
return
|
|
15048
|
+
return DEFAULT_CONTEXT_LIMIT2;
|
|
14779
15049
|
}
|
|
14780
15050
|
var WARNING_COOLDOWN_MS = 120000;
|
|
14781
15051
|
function createContextWindowMonitorHook(ctx) {
|
|
@@ -14825,7 +15095,7 @@ function createContextWindowMonitorHook(ctx) {
|
|
|
14825
15095
|
const cacheRead = usage?.cacheReadInputTokens || 0;
|
|
14826
15096
|
const totalUsed = inputTokens + cacheRead;
|
|
14827
15097
|
const modelID = info.modelID || "";
|
|
14828
|
-
const contextLimit =
|
|
15098
|
+
const contextLimit = getContextLimit(modelID);
|
|
14829
15099
|
const usageRatio = totalUsed / contextLimit;
|
|
14830
15100
|
state.lastUsageRatio.set(sessionID, usageRatio);
|
|
14831
15101
|
if (usageRatio >= WARNING_THRESHOLD) {
|
|
@@ -14907,7 +15177,7 @@ function analyzeComments(content) {
|
|
|
14907
15177
|
}
|
|
14908
15178
|
return issues;
|
|
14909
15179
|
}
|
|
14910
|
-
function createCommentCheckerHook(
|
|
15180
|
+
function createCommentCheckerHook(_ctx) {
|
|
14911
15181
|
return {
|
|
14912
15182
|
"tool.execute.after": async (input, output) => {
|
|
14913
15183
|
if (input.tool !== "Edit" && input.tool !== "edit")
|
|
@@ -14933,6 +15203,373 @@ Comments should explain WHY, not WHAT. Consider removing obvious comments.`;
|
|
|
14933
15203
|
};
|
|
14934
15204
|
}
|
|
14935
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
|
+
|
|
14936
15573
|
// src/tools/background-task/manager.ts
|
|
14937
15574
|
var POLL_INTERVAL_MS = 2000;
|
|
14938
15575
|
function generateTaskId() {
|
|
@@ -15103,7 +15740,7 @@ ${task.error}
|
|
|
15103
15740
|
`;
|
|
15104
15741
|
}
|
|
15105
15742
|
if (task.progress?.lastMessage) {
|
|
15106
|
-
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;
|
|
15107
15744
|
output += `
|
|
15108
15745
|
### Last Message Preview
|
|
15109
15746
|
${preview}
|
|
@@ -15343,6 +15980,18 @@ var MCP_SERVERS = {
|
|
|
15343
15980
|
command: ["npx", "-y", "@upstash/context7-mcp@latest"]
|
|
15344
15981
|
}
|
|
15345
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
|
+
}
|
|
15346
15995
|
var OpenCodeConfigPlugin = async (ctx) => {
|
|
15347
15996
|
const astGrepStatus = await checkAstGrepAvailable();
|
|
15348
15997
|
if (!astGrepStatus.available) {
|
|
@@ -15351,11 +16000,13 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15351
16000
|
const thinkModeState = new Map;
|
|
15352
16001
|
const autoCompactHook = createAutoCompactHook(ctx);
|
|
15353
16002
|
const contextInjectorHook = createContextInjectorHook(ctx);
|
|
15354
|
-
const
|
|
16003
|
+
const autoClearLedgerHook = createAutoClearLedgerHook(ctx);
|
|
16004
|
+
const ledgerLoaderHook = createLedgerLoaderHook(ctx);
|
|
15355
16005
|
const sessionRecoveryHook = createSessionRecoveryHook(ctx);
|
|
15356
16006
|
const tokenAwareTruncationHook = createTokenAwareTruncationHook(ctx);
|
|
15357
16007
|
const contextWindowMonitorHook = createContextWindowMonitorHook(ctx);
|
|
15358
16008
|
const commentCheckerHook = createCommentCheckerHook(ctx);
|
|
16009
|
+
const artifactAutoIndexHook = createArtifactAutoIndexHook(ctx);
|
|
15359
16010
|
const backgroundTaskManager = new BackgroundTaskManager(ctx);
|
|
15360
16011
|
const backgroundTaskTools = createBackgroundTaskTools(backgroundTaskManager);
|
|
15361
16012
|
return {
|
|
@@ -15363,6 +16014,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15363
16014
|
ast_grep_search,
|
|
15364
16015
|
ast_grep_replace,
|
|
15365
16016
|
look_at,
|
|
16017
|
+
artifact_search,
|
|
15366
16018
|
...backgroundTaskTools
|
|
15367
16019
|
},
|
|
15368
16020
|
config: async (config2) => {
|
|
@@ -15391,6 +16043,16 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15391
16043
|
description: "Initialize project with ARCHITECTURE.md and CODE_STYLE.md",
|
|
15392
16044
|
agent: "project-initializer",
|
|
15393
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`
|
|
15394
16056
|
}
|
|
15395
16057
|
};
|
|
15396
16058
|
},
|
|
@@ -15399,6 +16061,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15399
16061
|
thinkModeState.set(input.sessionID, detectThinkKeyword(text));
|
|
15400
16062
|
},
|
|
15401
16063
|
"chat.params": async (input, output) => {
|
|
16064
|
+
await ledgerLoaderHook["chat.params"](input, output);
|
|
15402
16065
|
await contextInjectorHook["chat.params"](input, output);
|
|
15403
16066
|
await contextWindowMonitorHook["chat.params"](input, output);
|
|
15404
16067
|
if (thinkModeState.get(input.sessionID)) {
|
|
@@ -15415,6 +16078,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15415
16078
|
await tokenAwareTruncationHook["tool.execute.after"]({ name: input.tool, sessionID: input.sessionID }, output);
|
|
15416
16079
|
await commentCheckerHook["tool.execute.after"]({ tool: input.tool, args: input.args }, output);
|
|
15417
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);
|
|
15418
16082
|
},
|
|
15419
16083
|
event: async ({ event }) => {
|
|
15420
16084
|
if (event.type === "session.deleted") {
|
|
@@ -15424,7 +16088,7 @@ var OpenCodeConfigPlugin = async (ctx) => {
|
|
|
15424
16088
|
}
|
|
15425
16089
|
}
|
|
15426
16090
|
await autoCompactHook.event({ event });
|
|
15427
|
-
await
|
|
16091
|
+
await autoClearLedgerHook.event({ event });
|
|
15428
16092
|
await sessionRecoveryHook.event({ event });
|
|
15429
16093
|
await tokenAwareTruncationHook.event({ event });
|
|
15430
16094
|
await contextWindowMonitorHook.event({ event });
|