clawvault 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/register-core-commands.js +25 -2
- package/bin/register-task-commands.js +18 -2
- package/bin/register-task-commands.test.js +24 -0
- package/dist/{chunk-TB3BM2PQ.js → chunk-33GW63WK.js} +4 -35
- package/dist/{chunk-JVAWKNIZ.js → chunk-AHGUJG76.js} +1 -1
- package/dist/{chunk-TT3FXYCN.js → chunk-BI6SGGZP.js} +1 -1
- package/dist/{chunk-NGVAEFT2.js → chunk-DEFBIVQ3.js} +21 -0
- package/dist/{chunk-USZU5CBB.js → chunk-DHJPXGC7.js} +1 -1
- package/dist/{chunk-2AYPFUGX.js → chunk-FEFPBHH4.js} +286 -11
- package/dist/{chunk-K6XHCUFL.js → chunk-FHFUXL6G.js} +8 -1
- package/dist/{chunk-RARDNTUP.js → chunk-GBIDDDSL.js} +2 -2
- package/dist/chunk-IFTEGE4D.js +361 -0
- package/dist/{chunk-VBVEXNI5.js → chunk-JXY6T5R7.js} +1 -1
- package/dist/chunk-L3DJ36BZ.js +40 -0
- package/dist/{chunk-OTQW3OMC.js → chunk-Q3WBH4P4.js} +97 -0
- package/dist/{chunk-6AQZIPLV.js → chunk-SNEMCQP7.js} +12 -5
- package/dist/commands/backlog.js +9 -2
- package/dist/commands/blocked.js +9 -2
- package/dist/commands/canvas.d.ts +11 -3
- package/dist/commands/canvas.js +954 -20
- package/dist/commands/context.js +4 -3
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/migrate-observations.js +2 -2
- package/dist/commands/observe.d.ts +1 -0
- package/dist/commands/observe.js +4 -3
- package/dist/commands/rebuild.js +4 -3
- package/dist/commands/reflect.js +3 -3
- package/dist/commands/replay.js +5 -4
- package/dist/commands/setup.d.ts +10 -2
- package/dist/commands/setup.js +1 -1
- package/dist/commands/sleep.js +5 -4
- package/dist/commands/status.js +1 -1
- package/dist/commands/task.js +1 -1
- package/dist/commands/wake.js +3 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.js +13 -11
- package/dist/lib/task-utils.d.ts +13 -1
- package/dist/lib/task-utils.js +3 -1
- package/package.json +1 -1
- package/dist/chunk-W463YRED.js +0 -97
|
@@ -13,6 +13,13 @@ export function registerCoreCommands(
|
|
|
13
13
|
.option('-n, --name <name>', 'Vault name')
|
|
14
14
|
.option('--qmd', 'Set up qmd semantic search collection')
|
|
15
15
|
.option('--qmd-collection <name>', 'qmd collection name (defaults to vault name)')
|
|
16
|
+
.option('--no-bases', 'Skip Obsidian Bases file generation')
|
|
17
|
+
.option('--no-tasks', 'Skip tasks/ and backlog/ directories')
|
|
18
|
+
.option('--no-graph', 'Skip initial graph build')
|
|
19
|
+
.option('--categories <list>', 'Comma-separated list of custom categories to create')
|
|
20
|
+
.option('--canvas <template>', 'Generate a canvas dashboard on init (default, brain, project-board, sprint)')
|
|
21
|
+
.option('--theme <style>', 'Graph color theme to apply (neural, minimal, none)', 'none')
|
|
22
|
+
.option('--minimal', 'Create minimal vault (memory categories only, no tasks/bases/graph)')
|
|
16
23
|
.action(async (vaultPath, options) => {
|
|
17
24
|
const targetPath = vaultPath || '.';
|
|
18
25
|
console.log(chalk.cyan(`\n🐘 Initializing ClawVault at ${path.resolve(targetPath)}...\n`));
|
|
@@ -67,10 +74,26 @@ export function registerCoreCommands(
|
|
|
67
74
|
program
|
|
68
75
|
.command('setup')
|
|
69
76
|
.description('Auto-discover and configure a ClawVault')
|
|
70
|
-
.
|
|
77
|
+
.option('--graph-colors', 'Set up graph color scheme for Obsidian')
|
|
78
|
+
.option('--no-graph-colors', 'Skip graph color configuration')
|
|
79
|
+
.option('--bases', 'Generate Obsidian Bases views for task management')
|
|
80
|
+
.option('--no-bases', 'Skip Bases file generation')
|
|
81
|
+
.option('--canvas [template]', 'Generate canvas dashboard (default, brain, project-board, sprint)')
|
|
82
|
+
.option('--no-canvas', 'Skip canvas generation')
|
|
83
|
+
.option('--theme <style>', 'Graph color theme (neural, minimal, none)', 'neural')
|
|
84
|
+
.option('--force', 'Overwrite existing configuration files')
|
|
85
|
+
.option('-v, --vault <path>', 'Vault path')
|
|
86
|
+
.action(async (options) => {
|
|
71
87
|
try {
|
|
72
88
|
const { setupCommand } = await import('../dist/commands/setup.js');
|
|
73
|
-
await setupCommand(
|
|
89
|
+
await setupCommand({
|
|
90
|
+
graphColors: options.graphColors,
|
|
91
|
+
bases: options.bases,
|
|
92
|
+
canvas: options.canvas,
|
|
93
|
+
theme: options.theme,
|
|
94
|
+
force: options.force,
|
|
95
|
+
vault: options.vault
|
|
96
|
+
});
|
|
74
97
|
} catch (err) {
|
|
75
98
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
76
99
|
process.exit(1);
|
|
@@ -242,12 +242,28 @@ export function registerTaskCommands(
|
|
|
242
242
|
.description('Generate Obsidian canvas dashboard')
|
|
243
243
|
.option('-v, --vault <path>', 'Vault path')
|
|
244
244
|
.option('--output <path>', 'Output file path (default: dashboard.canvas)')
|
|
245
|
+
.option('--template <id>', 'Canvas template ID (default, project-board, brain, sprint)')
|
|
246
|
+
.option('--project <project>', 'Project filter for template-aware canvases')
|
|
247
|
+
.option('--owner <owner>', 'Filter tasks by owner (agent name or human)')
|
|
248
|
+
.option('--width <pixels>', 'Canvas width in pixels', parseInt)
|
|
249
|
+
.option('--height <pixels>', 'Canvas height in pixels', parseInt)
|
|
250
|
+
.option('--include-done', 'Include completed tasks (default: limited)')
|
|
251
|
+
.option('--list-templates', 'List available canvas templates and exit')
|
|
245
252
|
.action(async (options) => {
|
|
246
253
|
try {
|
|
247
|
-
const vaultPath =
|
|
254
|
+
const vaultPath = options.listTemplates
|
|
255
|
+
? (options.vault || '.')
|
|
256
|
+
: resolveVaultPath(options.vault);
|
|
248
257
|
const { canvasCommand } = await import('../dist/commands/canvas.js');
|
|
249
258
|
await canvasCommand(vaultPath, {
|
|
250
|
-
output: options.output
|
|
259
|
+
output: options.output,
|
|
260
|
+
template: options.template,
|
|
261
|
+
project: options.project,
|
|
262
|
+
owner: options.owner,
|
|
263
|
+
width: options.width,
|
|
264
|
+
height: options.height,
|
|
265
|
+
includeDone: options.includeDone,
|
|
266
|
+
listTemplates: options.listTemplates
|
|
251
267
|
});
|
|
252
268
|
} catch (err) {
|
|
253
269
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerTaskCommands } from './register-task-commands.js';
|
|
4
|
+
import { chalkStub, stubResolveVaultPath } from './test-helpers/cli-command-fixtures.js';
|
|
5
|
+
|
|
6
|
+
describe('register-task-commands', () => {
|
|
7
|
+
it('adds canvas template and listing flags', () => {
|
|
8
|
+
const program = new Command();
|
|
9
|
+
registerTaskCommands(program, {
|
|
10
|
+
chalk: chalkStub,
|
|
11
|
+
resolveVaultPath: stubResolveVaultPath
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const canvasCommand = program.commands.find((command) => command.name() === 'canvas');
|
|
15
|
+
expect(canvasCommand).toBeDefined();
|
|
16
|
+
|
|
17
|
+
const optionFlags = canvasCommand?.options.map((option) => option.flags) ?? [];
|
|
18
|
+
expect(optionFlags).toEqual(expect.arrayContaining([
|
|
19
|
+
'--template <id>',
|
|
20
|
+
'--list-templates',
|
|
21
|
+
'--project <project>'
|
|
22
|
+
]));
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -1,48 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ClawVault
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-Q3WBH4P4.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
parseObservationLines,
|
|
6
|
+
readObservations
|
|
7
|
+
} from "./chunk-L3DJ36BZ.js";
|
|
7
8
|
import {
|
|
8
9
|
getMemoryGraph
|
|
9
10
|
} from "./chunk-ZZA73MFY.js";
|
|
10
|
-
import {
|
|
11
|
-
listObservationFiles
|
|
12
|
-
} from "./chunk-NAMFB7ZA.js";
|
|
13
11
|
|
|
14
12
|
// src/commands/context.ts
|
|
15
13
|
import * as path from "path";
|
|
16
14
|
|
|
17
|
-
// src/lib/observation-reader.ts
|
|
18
|
-
import * as fs from "fs";
|
|
19
|
-
function readObservations(vaultPath, days = 7) {
|
|
20
|
-
const normalizedDays = Number.isFinite(days) ? Math.max(0, Math.floor(days)) : 0;
|
|
21
|
-
if (normalizedDays === 0) {
|
|
22
|
-
return "";
|
|
23
|
-
}
|
|
24
|
-
const files = listObservationFiles(vaultPath, {
|
|
25
|
-
includeLegacy: true,
|
|
26
|
-
includeArchive: false,
|
|
27
|
-
dedupeByDate: true
|
|
28
|
-
}).sort((left, right) => right.date.localeCompare(left.date)).slice(0, normalizedDays);
|
|
29
|
-
if (files.length === 0) {
|
|
30
|
-
return "";
|
|
31
|
-
}
|
|
32
|
-
return files.map((entry) => fs.readFileSync(entry.path, "utf-8").trim()).filter(Boolean).join("\n\n").trim();
|
|
33
|
-
}
|
|
34
|
-
function parseObservationLines(markdown) {
|
|
35
|
-
return parseObservationMarkdown(markdown).map((record) => ({
|
|
36
|
-
type: record.type,
|
|
37
|
-
confidence: record.confidence,
|
|
38
|
-
importance: record.importance,
|
|
39
|
-
content: record.content,
|
|
40
|
-
date: record.date,
|
|
41
|
-
format: record.format,
|
|
42
|
-
priority: record.priority
|
|
43
|
-
}));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
15
|
// src/lib/token-counter.ts
|
|
47
16
|
function estimateTokens(text) {
|
|
48
17
|
if (!text) {
|
|
@@ -147,6 +147,7 @@ function createTask(vaultPath, title, options = {}) {
|
|
|
147
147
|
created: now,
|
|
148
148
|
updated: now
|
|
149
149
|
};
|
|
150
|
+
if (options.source) frontmatter.source = options.source;
|
|
150
151
|
if (options.owner) frontmatter.owner = options.owner;
|
|
151
152
|
if (options.project) frontmatter.project = options.project;
|
|
152
153
|
if (options.priority) frontmatter.priority = options.priority;
|
|
@@ -264,6 +265,25 @@ ${options.content}
|
|
|
264
265
|
path: backlogPath
|
|
265
266
|
};
|
|
266
267
|
}
|
|
268
|
+
function updateBacklogItem(vaultPath, slug, updates) {
|
|
269
|
+
const backlogItem = readBacklogItem(vaultPath, slug);
|
|
270
|
+
if (!backlogItem) {
|
|
271
|
+
throw new Error(`Backlog item not found: ${slug}`);
|
|
272
|
+
}
|
|
273
|
+
const newFrontmatter = {
|
|
274
|
+
...backlogItem.frontmatter
|
|
275
|
+
};
|
|
276
|
+
if (updates.source !== void 0) newFrontmatter.source = updates.source;
|
|
277
|
+
if (updates.project !== void 0) newFrontmatter.project = updates.project;
|
|
278
|
+
if (updates.tags !== void 0) newFrontmatter.tags = updates.tags;
|
|
279
|
+
if (updates.lastSeen !== void 0) newFrontmatter.lastSeen = updates.lastSeen;
|
|
280
|
+
const fileContent = matter.stringify(backlogItem.content, newFrontmatter);
|
|
281
|
+
fs.writeFileSync(backlogItem.path, fileContent);
|
|
282
|
+
return {
|
|
283
|
+
...backlogItem,
|
|
284
|
+
frontmatter: newFrontmatter
|
|
285
|
+
};
|
|
286
|
+
}
|
|
267
287
|
function promoteBacklogItem(vaultPath, slug, options = {}) {
|
|
268
288
|
const backlogItem = readBacklogItem(vaultPath, slug);
|
|
269
289
|
if (!backlogItem) {
|
|
@@ -343,6 +363,7 @@ export {
|
|
|
343
363
|
updateTask,
|
|
344
364
|
completeTask,
|
|
345
365
|
createBacklogItem,
|
|
366
|
+
updateBacklogItem,
|
|
346
367
|
promoteBacklogItem,
|
|
347
368
|
getBlockedTasks,
|
|
348
369
|
getActiveTasks,
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBacklogItem,
|
|
3
|
+
listBacklogItems,
|
|
4
|
+
listTasks,
|
|
5
|
+
updateBacklogItem,
|
|
6
|
+
updateTask
|
|
7
|
+
} from "./chunk-DEFBIVQ3.js";
|
|
1
8
|
import {
|
|
2
9
|
DATE_HEADING_RE,
|
|
3
10
|
inferObservationType,
|
|
@@ -6,7 +13,7 @@ import {
|
|
|
6
13
|
parseObservationMarkdown,
|
|
7
14
|
renderObservationMarkdown,
|
|
8
15
|
renderScoredObservationLine
|
|
9
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-FHFUXL6G.js";
|
|
10
17
|
import {
|
|
11
18
|
ensureLedgerStructure,
|
|
12
19
|
ensureParentDir,
|
|
@@ -20,6 +27,10 @@ import {
|
|
|
20
27
|
var CRITICAL_RE = /(?:\b(?:decision|decided|chose|chosen|selected|picked|opted|switched to)\s*:?|\bdecid(?:e|ed|ing|ion)\b|\berror\b|\bfail(?:ed|ure|ing)?\b|\bblock(?:ed|er)?\b|\bbreaking(?:\s+change)?s?\b|\bcritical\b|\b\w+\s+chosen\s+(?:for|over|as)\b|\bpublish(?:ed)?\b.*@?\d+\.\d+|\bmerge[d]?\s+(?:PR|pull\s+request)\b|\bshipped\b|\breleased?\b.*v?\d+\.\d+|\bsigned\b.*\b(?:contract|agreement|deal)\b|\bpricing\b.*\$|\bdemo\b.*\b(?:completed?|done|finished)\b|\bmeeting\b.*\b(?:completed?|done|finished)\b|\bstrategy\b.*\b(?:pivot|change|shift)\b)/i;
|
|
21
28
|
var DEADLINE_WITH_DATE_RE = /(?:(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b).*(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2})|(?:\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?|(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+\d{1,2}).*(?:\bdeadline\b|\bdue(?:\s+date)?\b|\bcutoff\b))/i;
|
|
22
29
|
var NOTABLE_RE = /\b(prefer(?:ence|s)?|likes?|dislikes?|context|pattern|architecture|approach|trade[- ]?off|milestone|stakeholder|teammate|collaborat(?:e|ed|ion)|discussion|notable|deadline|due|timeline|deploy(?:ed|ment)?|built|configured|launched|proposal|pitch|onboard(?:ed|ing)?|migrat(?:e|ed|ion)|domain|DNS|infra(?:structure)?)\b/i;
|
|
30
|
+
var TODO_SIGNAL_RE = /(?:\btodo:\s*|\bwe need to\b|\bdon't forget(?: to)?\b|\bremember to\b|\bmake sure to\b)/i;
|
|
31
|
+
var COMMITMENT_TASK_SIGNAL_RE = /\b(?:i'?ll|i will|let me|(?:i'?m\s+)?going to|plan to|should)\b/i;
|
|
32
|
+
var UNRESOLVED_COMMITMENT_RE = /\b(?:need to figure out|tbd|to be determined)\b/i;
|
|
33
|
+
var DEADLINE_SIGNAL_RE = /\b(?:by\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow)|before\s+the\s+\w+|deadline is)\b/i;
|
|
23
34
|
var Compressor = class {
|
|
24
35
|
model;
|
|
25
36
|
now;
|
|
@@ -70,13 +81,19 @@ var Compressor = class {
|
|
|
70
81
|
"- Output markdown only.",
|
|
71
82
|
"- Group observations by date heading: ## YYYY-MM-DD",
|
|
72
83
|
"- Each observation line MUST follow: - [type|c=<0.00-1.00>|i=<0.00-1.00>] <observation>",
|
|
73
|
-
"- Allowed type tags: decision, preference, fact, commitment, milestone, lesson, relationship, project",
|
|
84
|
+
"- Allowed type tags: decision, preference, fact, commitment, task, todo, commitment-unresolved, milestone, lesson, relationship, project",
|
|
74
85
|
"- i >= 0.80 for structural/persistent observations (major decisions, blockers, releases, commitments)",
|
|
75
86
|
"- i 0.40-0.79 for potentially important observations (notable context, preferences, milestones)",
|
|
76
87
|
"- i < 0.40 for contextual/routine observations",
|
|
77
88
|
"- Confidence c reflects extraction certainty, not importance.",
|
|
78
89
|
"- Preserve source tags when present (e.g., [main], [telegram-dm], [discord], [telegram-group]).",
|
|
79
90
|
"",
|
|
91
|
+
"TASK EXTRACTION (required):",
|
|
92
|
+
`- Emit [todo] for explicit TODO phrasing: "TODO:", "we need to", "don't forget", "remember to", "make sure to".`,
|
|
93
|
+
`- Emit [task] for commitments/action intent: "I'll", "I will", "let me", "going to", "plan to", "should".`,
|
|
94
|
+
'- Emit [commitment-unresolved] for unresolved commitments/questions: "need to figure out", "TBD", "to be determined".',
|
|
95
|
+
'- Deadline language ("by Friday", "before the demo", "deadline is") should increase importance and usually map to [task] unless unresolved.',
|
|
96
|
+
"",
|
|
80
97
|
"QUALITY FILTERS (important):",
|
|
81
98
|
"- DO NOT observe: CLI errors, command failures, tool output parsing issues, retry attempts, debug logs.",
|
|
82
99
|
" These are transient noise, not memories. Only observe errors if they represent a BLOCKER or an unresolved problem.",
|
|
@@ -236,6 +253,7 @@ ${cleaned}`;
|
|
|
236
253
|
let importance = record.importance;
|
|
237
254
|
let confidence = record.confidence;
|
|
238
255
|
let type = record.type;
|
|
256
|
+
const inferredTaskType = this.inferTaskType(record.content);
|
|
239
257
|
if (this.isCriticalContent(record.content)) {
|
|
240
258
|
importance = Math.max(importance, 0.85);
|
|
241
259
|
confidence = Math.max(confidence, 0.85);
|
|
@@ -246,6 +264,11 @@ ${cleaned}`;
|
|
|
246
264
|
importance = Math.max(importance, 0.5);
|
|
247
265
|
confidence = Math.max(confidence, 0.75);
|
|
248
266
|
}
|
|
267
|
+
if (inferredTaskType) {
|
|
268
|
+
type = type === "fact" || type === "commitment" ? inferredTaskType : type;
|
|
269
|
+
importance = Math.max(importance, inferredTaskType === "commitment-unresolved" ? 0.72 : 0.65);
|
|
270
|
+
confidence = Math.max(confidence, 0.8);
|
|
271
|
+
}
|
|
249
272
|
if (type === "decision" || type === "commitment" || type === "milestone") {
|
|
250
273
|
importance = Math.max(importance, 0.6);
|
|
251
274
|
}
|
|
@@ -340,16 +363,21 @@ ${cleaned}`;
|
|
|
340
363
|
return renderObservationMarkdown(sections);
|
|
341
364
|
}
|
|
342
365
|
inferImportance(text, type) {
|
|
366
|
+
const inferredTaskType = this.inferTaskType(text);
|
|
343
367
|
if (this.isCriticalContent(text)) return 0.9;
|
|
368
|
+
if (inferredTaskType === "commitment-unresolved") return 0.72;
|
|
369
|
+
if (inferredTaskType === "task" || inferredTaskType === "todo") return 0.65;
|
|
344
370
|
if (this.isNotableContent(text)) return 0.6;
|
|
345
371
|
if (type === "decision" || type === "commitment" || type === "milestone") return 0.55;
|
|
346
372
|
if (type === "preference" || type === "lesson" || type === "relationship" || type === "project") return 0.45;
|
|
347
373
|
return 0.2;
|
|
348
374
|
}
|
|
349
375
|
inferConfidence(text, type, importance) {
|
|
376
|
+
const inferredTaskType = this.inferTaskType(text);
|
|
350
377
|
let confidence = 0.72;
|
|
351
378
|
if (importance >= 0.8) confidence += 0.12;
|
|
352
379
|
if (type === "decision" || type === "commitment" || type === "milestone") confidence += 0.06;
|
|
380
|
+
if (inferredTaskType) confidence += 0.06;
|
|
353
381
|
if (/\b(?:decided|chose|committed|deadline|released|merged)\b/i.test(text)) {
|
|
354
382
|
confidence += 0.05;
|
|
355
383
|
}
|
|
@@ -361,6 +389,18 @@ ${cleaned}`;
|
|
|
361
389
|
isNotableContent(text) {
|
|
362
390
|
return NOTABLE_RE.test(text);
|
|
363
391
|
}
|
|
392
|
+
inferTaskType(text) {
|
|
393
|
+
if (UNRESOLVED_COMMITMENT_RE.test(text)) {
|
|
394
|
+
return "commitment-unresolved";
|
|
395
|
+
}
|
|
396
|
+
if (TODO_SIGNAL_RE.test(text)) {
|
|
397
|
+
return "todo";
|
|
398
|
+
}
|
|
399
|
+
if (COMMITMENT_TASK_SIGNAL_RE.test(text) || DEADLINE_SIGNAL_RE.test(text)) {
|
|
400
|
+
return "task";
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
364
404
|
normalizeText(text) {
|
|
365
405
|
return text.replace(/\s+/g, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim().slice(0, 280);
|
|
366
406
|
}
|
|
@@ -557,6 +597,9 @@ var TYPE_TO_CATEGORY = {
|
|
|
557
597
|
preference: "preferences",
|
|
558
598
|
fact: "facts",
|
|
559
599
|
commitment: "commitments",
|
|
600
|
+
task: "commitments",
|
|
601
|
+
todo: "commitments",
|
|
602
|
+
"commitment-unresolved": "commitments",
|
|
560
603
|
milestone: "projects",
|
|
561
604
|
lesson: "lessons",
|
|
562
605
|
relationship: "people",
|
|
@@ -564,19 +607,35 @@ var TYPE_TO_CATEGORY = {
|
|
|
564
607
|
};
|
|
565
608
|
var Router = class {
|
|
566
609
|
vaultPath;
|
|
567
|
-
|
|
610
|
+
extractTasks;
|
|
611
|
+
now;
|
|
612
|
+
constructor(vaultPath, options = {}) {
|
|
568
613
|
this.vaultPath = path.resolve(vaultPath);
|
|
614
|
+
this.extractTasks = options.extractTasks ?? true;
|
|
615
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
569
616
|
}
|
|
570
617
|
/**
|
|
571
618
|
* Takes observation markdown and routes items to appropriate vault categories.
|
|
572
619
|
* Routes only items with importance >= 0.4.
|
|
573
620
|
* Returns a summary of what was routed where.
|
|
574
621
|
*/
|
|
575
|
-
route(observationMarkdown) {
|
|
622
|
+
route(observationMarkdown, context = {}) {
|
|
576
623
|
const items = this.parseObservations(observationMarkdown);
|
|
577
624
|
const routed = [];
|
|
625
|
+
const knownWorkItems = this.extractTasks ? this.loadExistingWorkItems() : [];
|
|
626
|
+
let dedupHits = 0;
|
|
578
627
|
for (const item of items) {
|
|
579
628
|
if (item.importance < 0.4) continue;
|
|
629
|
+
if (this.extractTasks && this.isTaskObservation(item.type)) {
|
|
630
|
+
const taskResult = this.routeTaskObservation(item, context, knownWorkItems);
|
|
631
|
+
if (taskResult.routedItem) {
|
|
632
|
+
routed.push(taskResult.routedItem);
|
|
633
|
+
}
|
|
634
|
+
if (taskResult.dedupHit) {
|
|
635
|
+
dedupHits += 1;
|
|
636
|
+
}
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
580
639
|
const category = this.categorize(item.type, item.content);
|
|
581
640
|
if (!category) continue;
|
|
582
641
|
const routedItem = {
|
|
@@ -591,9 +650,202 @@ var Router = class {
|
|
|
591
650
|
routed.push(routedItem);
|
|
592
651
|
this.appendToCategory(category, routedItem);
|
|
593
652
|
}
|
|
594
|
-
const summary = this.buildSummary(routed);
|
|
653
|
+
const summary = this.buildSummary(routed, dedupHits);
|
|
595
654
|
return { routed, summary };
|
|
596
655
|
}
|
|
656
|
+
isTaskObservation(type) {
|
|
657
|
+
return type === "task" || type === "todo" || type === "commitment-unresolved";
|
|
658
|
+
}
|
|
659
|
+
routeTaskObservation(item, context, knownWorkItems) {
|
|
660
|
+
const title = this.deriveTaskTitle(item.content, item.type);
|
|
661
|
+
if (!title) {
|
|
662
|
+
return { routedItem: null, dedupHit: false };
|
|
663
|
+
}
|
|
664
|
+
const duplicate = this.findDuplicateWorkItem(title, knownWorkItems);
|
|
665
|
+
if (duplicate) {
|
|
666
|
+
if (item.type === "commitment-unresolved" && this.isOpenWorkItem(duplicate)) {
|
|
667
|
+
this.touchExistingWorkItem(duplicate);
|
|
668
|
+
}
|
|
669
|
+
console.log(`[observer] dedup hit for task candidate: "${title}"`);
|
|
670
|
+
return { routedItem: null, dedupHit: true };
|
|
671
|
+
}
|
|
672
|
+
const tags = this.mergeTags(
|
|
673
|
+
["open", "observer"],
|
|
674
|
+
item.type === "task" ? ["task"] : [],
|
|
675
|
+
item.type === "todo" ? ["todo"] : [],
|
|
676
|
+
item.type === "commitment-unresolved" ? ["commitment"] : []
|
|
677
|
+
);
|
|
678
|
+
const content = this.buildTaskContextContent(item, context);
|
|
679
|
+
let backlogItem;
|
|
680
|
+
try {
|
|
681
|
+
backlogItem = createBacklogItem(this.vaultPath, title, {
|
|
682
|
+
source: "observer",
|
|
683
|
+
content,
|
|
684
|
+
tags
|
|
685
|
+
});
|
|
686
|
+
} catch (error) {
|
|
687
|
+
if (error instanceof Error && /already exists/i.test(error.message)) {
|
|
688
|
+
console.log(`[observer] dedup hit for task candidate: "${title}"`);
|
|
689
|
+
return { routedItem: null, dedupHit: true };
|
|
690
|
+
}
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
knownWorkItems.push({
|
|
694
|
+
kind: "backlog",
|
|
695
|
+
slug: backlogItem.slug,
|
|
696
|
+
title: backlogItem.title,
|
|
697
|
+
status: "open",
|
|
698
|
+
source: backlogItem.frontmatter.source,
|
|
699
|
+
tags: backlogItem.frontmatter.tags ?? []
|
|
700
|
+
});
|
|
701
|
+
return {
|
|
702
|
+
dedupHit: false,
|
|
703
|
+
routedItem: {
|
|
704
|
+
category: "backlog",
|
|
705
|
+
title: backlogItem.title,
|
|
706
|
+
content: item.content,
|
|
707
|
+
type: item.type,
|
|
708
|
+
confidence: item.confidence,
|
|
709
|
+
importance: item.importance,
|
|
710
|
+
date: item.date
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
loadExistingWorkItems() {
|
|
715
|
+
const taskItems = listTasks(this.vaultPath).map((task) => ({
|
|
716
|
+
kind: "task",
|
|
717
|
+
slug: task.slug,
|
|
718
|
+
title: task.title,
|
|
719
|
+
status: task.frontmatter.status,
|
|
720
|
+
source: task.frontmatter.source,
|
|
721
|
+
tags: task.frontmatter.tags ?? []
|
|
722
|
+
}));
|
|
723
|
+
const backlogItems = listBacklogItems(this.vaultPath).map((item) => ({
|
|
724
|
+
kind: "backlog",
|
|
725
|
+
slug: item.slug,
|
|
726
|
+
title: item.title,
|
|
727
|
+
status: item.frontmatter.tags?.includes("done") ? "done" : "open",
|
|
728
|
+
source: item.frontmatter.source,
|
|
729
|
+
tags: item.frontmatter.tags ?? []
|
|
730
|
+
}));
|
|
731
|
+
return [...taskItems, ...backlogItems];
|
|
732
|
+
}
|
|
733
|
+
findDuplicateWorkItem(title, knownWorkItems) {
|
|
734
|
+
const normalizedTitle = this.normalizeTaskTitle(title);
|
|
735
|
+
if (!normalizedTitle) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
for (const item of knownWorkItems) {
|
|
739
|
+
const normalizedExisting = this.normalizeTaskTitle(item.title);
|
|
740
|
+
if (!normalizedExisting) {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (normalizedExisting === normalizedTitle) {
|
|
744
|
+
return item;
|
|
745
|
+
}
|
|
746
|
+
if (this.jaccardWordSimilarity(normalizedTitle, normalizedExisting) > 0.8) {
|
|
747
|
+
return item;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
normalizeTaskTitle(title) {
|
|
753
|
+
return title.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim().slice(0, 50);
|
|
754
|
+
}
|
|
755
|
+
jaccardWordSimilarity(a, b) {
|
|
756
|
+
const aWords = new Set(a.split(" ").filter(Boolean));
|
|
757
|
+
const bWords = new Set(b.split(" ").filter(Boolean));
|
|
758
|
+
if (aWords.size === 0 || bWords.size === 0) {
|
|
759
|
+
return 0;
|
|
760
|
+
}
|
|
761
|
+
let intersection = 0;
|
|
762
|
+
for (const word of aWords) {
|
|
763
|
+
if (bWords.has(word)) {
|
|
764
|
+
intersection += 1;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const unionSize = aWords.size + bWords.size - intersection;
|
|
768
|
+
return unionSize === 0 ? 0 : intersection / unionSize;
|
|
769
|
+
}
|
|
770
|
+
deriveTaskTitle(content, type) {
|
|
771
|
+
let title = content.replace(/^\d{2}:\d{2}\s+/, "").replace(/\[[^\]]+\]\s*/g, "").trim();
|
|
772
|
+
if (type === "todo") {
|
|
773
|
+
title = title.replace(
|
|
774
|
+
/^(?:todo:\s*|we need to\s+|don't forget(?: to)?\s+|remember to\s+|make sure to\s+)/i,
|
|
775
|
+
""
|
|
776
|
+
);
|
|
777
|
+
} else if (type === "task") {
|
|
778
|
+
title = title.replace(
|
|
779
|
+
/^(?:i'?ll\s+|i will\s+|let me\s+|(?:i'?m\s+)?going to\s+|plan to\s+|should\s+)/i,
|
|
780
|
+
""
|
|
781
|
+
);
|
|
782
|
+
} else if (type === "commitment-unresolved") {
|
|
783
|
+
title = title.replace(/^(?:need to figure out\s+|tbd[:\s-]*|to be determined[:\s-]*)/i, "");
|
|
784
|
+
}
|
|
785
|
+
title = title.replace(/\s+/g, " ").replace(/^[^a-zA-Z0-9]+/, "").replace(/[.?!:;,]+$/, "").trim();
|
|
786
|
+
return title.slice(0, 120);
|
|
787
|
+
}
|
|
788
|
+
buildTaskContextContent(item, context) {
|
|
789
|
+
const lines = ["Auto-extracted by observer from session transcript."];
|
|
790
|
+
if (context.sessionKey) {
|
|
791
|
+
lines.push(`Session: ${context.sessionKey}`);
|
|
792
|
+
}
|
|
793
|
+
if (context.transcriptId) {
|
|
794
|
+
lines.push(`Transcript: ${context.transcriptId}`);
|
|
795
|
+
}
|
|
796
|
+
if (context.source) {
|
|
797
|
+
lines.push(`Source: ${context.source}`);
|
|
798
|
+
}
|
|
799
|
+
const approximateTimestamp = this.extractApproximateTimestamp(item.date, item.content, context.timestamp);
|
|
800
|
+
lines.push(`Approximate timestamp: ${approximateTimestamp}`);
|
|
801
|
+
lines.push(`Observation type: ${item.type}`);
|
|
802
|
+
lines.push(`Original observation: ${item.content}`);
|
|
803
|
+
return lines.join("\n");
|
|
804
|
+
}
|
|
805
|
+
extractApproximateTimestamp(date, content, timestamp) {
|
|
806
|
+
if (timestamp) {
|
|
807
|
+
return timestamp.toISOString();
|
|
808
|
+
}
|
|
809
|
+
const timeMatch = content.match(/\b([01]\d|2[0-3]):([0-5]\d)\b/);
|
|
810
|
+
if (timeMatch) {
|
|
811
|
+
return `${date} ${timeMatch[0]}`;
|
|
812
|
+
}
|
|
813
|
+
return date;
|
|
814
|
+
}
|
|
815
|
+
isOpenWorkItem(item) {
|
|
816
|
+
if (item.kind === "task") {
|
|
817
|
+
return item.status !== "done";
|
|
818
|
+
}
|
|
819
|
+
return item.status !== "done";
|
|
820
|
+
}
|
|
821
|
+
touchExistingWorkItem(item) {
|
|
822
|
+
if (item.kind === "task") {
|
|
823
|
+
if (!this.isOpenWorkItem(item)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
updateTask(this.vaultPath, item.slug, {});
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const nextTags = this.mergeTags(item.tags, ["commitment"]);
|
|
830
|
+
updateBacklogItem(this.vaultPath, item.slug, {
|
|
831
|
+
source: item.source ?? "observer",
|
|
832
|
+
tags: nextTags,
|
|
833
|
+
lastSeen: this.now().toISOString()
|
|
834
|
+
});
|
|
835
|
+
item.tags = nextTags;
|
|
836
|
+
}
|
|
837
|
+
mergeTags(...groups) {
|
|
838
|
+
const merged = /* @__PURE__ */ new Set();
|
|
839
|
+
for (const group of groups) {
|
|
840
|
+
for (const tag of group) {
|
|
841
|
+
const normalized = tag.trim().toLowerCase();
|
|
842
|
+
if (normalized) {
|
|
843
|
+
merged.add(normalized);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return [...merged];
|
|
848
|
+
}
|
|
597
849
|
parseObservations(markdown) {
|
|
598
850
|
const records = parseObservationMarkdown(markdown);
|
|
599
851
|
return records.map((record) => ({
|
|
@@ -881,14 +1133,20 @@ ${entry}
|
|
|
881
1133
|
for (const bg of setA) if (setB.has(bg)) intersection++;
|
|
882
1134
|
return intersection / (setA.size + setB.size - intersection);
|
|
883
1135
|
}
|
|
884
|
-
buildSummary(routed) {
|
|
885
|
-
if (routed.length === 0)
|
|
1136
|
+
buildSummary(routed, dedupHits) {
|
|
1137
|
+
if (routed.length === 0) {
|
|
1138
|
+
if (dedupHits > 0) {
|
|
1139
|
+
return `No items routed to vault categories (dedup hits: ${dedupHits}).`;
|
|
1140
|
+
}
|
|
1141
|
+
return "No items routed to vault categories.";
|
|
1142
|
+
}
|
|
886
1143
|
const byCat = /* @__PURE__ */ new Map();
|
|
887
1144
|
for (const item of routed) {
|
|
888
1145
|
byCat.set(item.category, (byCat.get(item.category) ?? 0) + 1);
|
|
889
1146
|
}
|
|
890
1147
|
const parts = [...byCat.entries()].map(([cat, count]) => `${cat}: ${count}`);
|
|
891
|
-
|
|
1148
|
+
const suffix = dedupHits > 0 ? ` (dedup hits: ${dedupHits})` : "";
|
|
1149
|
+
return `Routed ${routed.length} observations \u2192 ${parts.join(", ")}${suffix}`;
|
|
892
1150
|
}
|
|
893
1151
|
};
|
|
894
1152
|
|
|
@@ -905,6 +1163,7 @@ var Observer = class {
|
|
|
905
1163
|
rawCapture;
|
|
906
1164
|
router;
|
|
907
1165
|
pendingMessages = [];
|
|
1166
|
+
pendingRouteContext = {};
|
|
908
1167
|
observationsCache = "";
|
|
909
1168
|
lastRoutingSummary = "";
|
|
910
1169
|
constructor(vaultPath, options = {}) {
|
|
@@ -915,7 +1174,10 @@ var Observer = class {
|
|
|
915
1174
|
this.compressor = options.compressor ?? new Compressor({ model: options.model, now: this.now });
|
|
916
1175
|
this.reflector = options.reflector ?? new Reflector({ now: this.now });
|
|
917
1176
|
this.rawCapture = options.rawCapture ?? true;
|
|
918
|
-
this.router = new Router(vaultPath
|
|
1177
|
+
this.router = new Router(vaultPath, {
|
|
1178
|
+
extractTasks: options.extractTasks,
|
|
1179
|
+
now: this.now
|
|
1180
|
+
});
|
|
919
1181
|
ensureLedgerStructure(this.vaultPath);
|
|
920
1182
|
this.observationsCache = this.readTodayObservations();
|
|
921
1183
|
}
|
|
@@ -928,6 +1190,7 @@ var Observer = class {
|
|
|
928
1190
|
this.persistRawMessages(incoming, options);
|
|
929
1191
|
}
|
|
930
1192
|
this.pendingMessages.push(...incoming);
|
|
1193
|
+
this.pendingRouteContext = this.mergeRouteContext(this.pendingRouteContext, options);
|
|
931
1194
|
const buffered = this.pendingMessages.join("\n");
|
|
932
1195
|
if (this.estimateTokens(buffered) < this.tokenThreshold) {
|
|
933
1196
|
return;
|
|
@@ -940,14 +1203,16 @@ var Observer = class {
|
|
|
940
1203
|
this.writeObservationFile(todayPath, existing);
|
|
941
1204
|
}
|
|
942
1205
|
const compressedRaw = (await this.compressor.compress(this.pendingMessages, existing)).trim();
|
|
1206
|
+
const routeContext = this.pendingRouteContext;
|
|
943
1207
|
this.pendingMessages = [];
|
|
1208
|
+
this.pendingRouteContext = {};
|
|
944
1209
|
const compressed = this.deduplicateObservationMarkdown(compressedRaw);
|
|
945
1210
|
if (!compressed) {
|
|
946
1211
|
return;
|
|
947
1212
|
}
|
|
948
1213
|
this.writeObservationFile(todayPath, compressed);
|
|
949
1214
|
this.observationsCache = compressed;
|
|
950
|
-
const { summary } = this.router.route(compressed);
|
|
1215
|
+
const { summary } = this.router.route(compressed, routeContext);
|
|
951
1216
|
if (summary) {
|
|
952
1217
|
this.lastRoutingSummary = summary;
|
|
953
1218
|
}
|
|
@@ -968,12 +1233,14 @@ var Observer = class {
|
|
|
968
1233
|
this.writeObservationFile(todayPath, existing);
|
|
969
1234
|
}
|
|
970
1235
|
const compressedRaw = (await this.compressor.compress(this.pendingMessages, existing)).trim();
|
|
1236
|
+
const routeContext = this.pendingRouteContext;
|
|
971
1237
|
this.pendingMessages = [];
|
|
1238
|
+
this.pendingRouteContext = {};
|
|
972
1239
|
const compressed = this.deduplicateObservationMarkdown(compressedRaw);
|
|
973
1240
|
if (compressed) {
|
|
974
1241
|
this.writeObservationFile(todayPath, compressed);
|
|
975
1242
|
this.observationsCache = compressed;
|
|
976
|
-
const { summary } = this.router.route(compressed);
|
|
1243
|
+
const { summary } = this.router.route(compressed, routeContext);
|
|
977
1244
|
this.lastRoutingSummary = summary;
|
|
978
1245
|
}
|
|
979
1246
|
return { observations: this.observationsCache, routingSummary: this.lastRoutingSummary };
|
|
@@ -1062,6 +1329,14 @@ var Observer = class {
|
|
|
1062
1329
|
}
|
|
1063
1330
|
return "openclaw";
|
|
1064
1331
|
}
|
|
1332
|
+
mergeRouteContext(existing, incoming) {
|
|
1333
|
+
const merged = { ...existing };
|
|
1334
|
+
if (incoming.source) merged.source = incoming.source;
|
|
1335
|
+
if (incoming.sessionKey) merged.sessionKey = incoming.sessionKey;
|
|
1336
|
+
if (incoming.transcriptId) merged.transcriptId = incoming.transcriptId;
|
|
1337
|
+
if (incoming.timestamp) merged.timestamp = incoming.timestamp;
|
|
1338
|
+
return merged;
|
|
1339
|
+
}
|
|
1065
1340
|
};
|
|
1066
1341
|
|
|
1067
1342
|
export {
|