invoket 0.1.8 → 0.1.9
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 +106 -140
- package/package.json +1 -1
- package/src/cli.ts +22 -0
- package/src/parser.ts +41 -0
package/README.md
CHANGED
|
@@ -300,180 +300,146 @@ EOF`);
|
|
|
300
300
|
|
|
301
301
|
## Agentic Tools
|
|
302
302
|
|
|
303
|
-
|
|
303
|
+
AI agents like Claude Code have built-in tools for searching files, reading code, and running commands. What they lack is **project context** — the state of migrations, the history of decisions, the shape of your API, which tests are flaky and why. That knowledge lives in developers' heads, scattered across commits, issues, and Slack threads.
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
invoket lets you build a **structured, queryable project knowledge base** that agents can read and write through the same CLI interface humans use. Bun's built-in SQLite makes this trivial — no external database, no setup, just a `.ctx.db` file that travels with the project.
|
|
306
306
|
|
|
307
|
-
|
|
308
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
309
|
-
|
|
310
|
-
class Memory {
|
|
311
|
-
private dir = ".memory";
|
|
312
|
-
|
|
313
|
-
private ensure() {
|
|
314
|
-
if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** Store a value by key */
|
|
318
|
-
async store(c: Context, key: string, ...value: string[]) {
|
|
319
|
-
this.ensure();
|
|
320
|
-
writeFileSync(`${this.dir}/${key}.md`, value.join(" "));
|
|
321
|
-
console.log(`Stored: ${key}`);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** Recall a value by key */
|
|
325
|
-
async recall(c: Context, key: string) {
|
|
326
|
-
const path = `${this.dir}/${key}.md`;
|
|
327
|
-
if (!existsSync(path)) { console.log(`Not found: ${key}`); return; }
|
|
328
|
-
console.log(readFileSync(path, "utf-8"));
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** List all stored keys */
|
|
332
|
-
async list(c: Context) {
|
|
333
|
-
this.ensure();
|
|
334
|
-
const { stdout } = await c.run(`ls ${this.dir}`, { hide: true, warn: true });
|
|
335
|
-
console.log(stdout || "(empty)");
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export class Tasks {
|
|
340
|
-
memory = new Memory();
|
|
341
|
-
}
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
```bash
|
|
345
|
-
invt memory:store arch "Monorepo with packages/api and packages/web"
|
|
346
|
-
invt memory:recall arch
|
|
347
|
-
invt memory:list
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### Task planning — break work into steps
|
|
307
|
+
### Project context — a SQLite-backed knowledge base
|
|
351
308
|
|
|
352
309
|
```typescript
|
|
353
|
-
import {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
310
|
+
import { Context } from "invoket/context";
|
|
311
|
+
import { Database } from "bun:sqlite";
|
|
312
|
+
|
|
313
|
+
class Ctx {
|
|
314
|
+
private db: Database;
|
|
315
|
+
|
|
316
|
+
constructor() {
|
|
317
|
+
this.db = new Database(".ctx.db", { create: true });
|
|
318
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS context (
|
|
319
|
+
key TEXT PRIMARY KEY,
|
|
320
|
+
value TEXT NOT NULL,
|
|
321
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
322
|
+
)`);
|
|
323
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS decisions (
|
|
324
|
+
id INTEGER PRIMARY KEY,
|
|
325
|
+
subject TEXT NOT NULL,
|
|
326
|
+
decision TEXT NOT NULL,
|
|
327
|
+
rationale TEXT,
|
|
328
|
+
status TEXT DEFAULT 'active',
|
|
329
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
330
|
+
)`);
|
|
331
|
+
}
|
|
357
332
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
333
|
+
/** Store a key-value fact about the project */
|
|
334
|
+
async set(c: Context, key: string, ...value: string[]) {
|
|
335
|
+
this.db.run(
|
|
336
|
+
`INSERT OR REPLACE INTO context (key, value, updated_at)
|
|
337
|
+
VALUES (?, ?, datetime('now'))`,
|
|
338
|
+
[key, value.join(" ")]
|
|
339
|
+
);
|
|
340
|
+
console.log(`Set: ${key}`);
|
|
361
341
|
}
|
|
362
342
|
|
|
363
|
-
|
|
364
|
-
|
|
343
|
+
/** Retrieve a fact */
|
|
344
|
+
async get(c: Context, key: string) {
|
|
345
|
+
const row = this.db.query("SELECT value, updated_at FROM context WHERE key = ?").get(key) as any;
|
|
346
|
+
if (!row) { console.log(`Not found: ${key}`); return; }
|
|
347
|
+
console.log(`${row.value} (${row.updated_at})`);
|
|
365
348
|
}
|
|
366
349
|
|
|
367
|
-
/**
|
|
368
|
-
async
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
350
|
+
/** Search facts by keyword */
|
|
351
|
+
async search(c: Context, ...terms: string[]) {
|
|
352
|
+
const pattern = `%${terms.join(" ")}%`;
|
|
353
|
+
const rows = this.db.query(
|
|
354
|
+
"SELECT key, value FROM context WHERE key LIKE ? OR value LIKE ?"
|
|
355
|
+
).all(pattern, pattern) as any[];
|
|
356
|
+
for (const r of rows) console.log(`${r.key}: ${r.value}`);
|
|
373
357
|
}
|
|
374
358
|
|
|
375
|
-
/**
|
|
376
|
-
async
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
359
|
+
/** Record an architectural decision */
|
|
360
|
+
async decide(c: Context, subject: string, decision: string, ...rationale: string[]) {
|
|
361
|
+
this.db.run(
|
|
362
|
+
"INSERT INTO decisions (subject, decision, rationale) VALUES (?, ?, ?)",
|
|
363
|
+
[subject, decision, rationale.join(" ")]
|
|
364
|
+
);
|
|
365
|
+
console.log(`Recorded: ${subject}`);
|
|
381
366
|
}
|
|
382
367
|
|
|
383
|
-
/**
|
|
384
|
-
async
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
368
|
+
/** List active decisions */
|
|
369
|
+
async decisions(c: Context) {
|
|
370
|
+
const rows = this.db.query(
|
|
371
|
+
"SELECT id, subject, decision, rationale FROM decisions WHERE status = 'active' ORDER BY created_at DESC"
|
|
372
|
+
).all() as any[];
|
|
373
|
+
for (const r of rows) {
|
|
374
|
+
console.log(`#${r.id} ${r.subject}: ${r.decision}`);
|
|
375
|
+
if (r.rationale) console.log(` ${r.rationale}`);
|
|
389
376
|
}
|
|
390
377
|
}
|
|
391
378
|
|
|
392
|
-
/**
|
|
393
|
-
async
|
|
394
|
-
this.
|
|
395
|
-
|
|
379
|
+
/** Dump all context as JSON */
|
|
380
|
+
async dump(c: Context) {
|
|
381
|
+
const facts = this.db.query("SELECT key, value FROM context ORDER BY key").all();
|
|
382
|
+
const decisions = this.db.query("SELECT * FROM decisions WHERE status = 'active'").all();
|
|
383
|
+
console.log(JSON.stringify({ facts, decisions }, null, 2));
|
|
396
384
|
}
|
|
397
385
|
}
|
|
398
386
|
|
|
399
387
|
export class Tasks {
|
|
400
|
-
|
|
388
|
+
ctx = new Ctx();
|
|
401
389
|
}
|
|
402
390
|
```
|
|
403
391
|
|
|
404
392
|
```bash
|
|
405
|
-
|
|
406
|
-
invt
|
|
407
|
-
invt
|
|
408
|
-
invt
|
|
409
|
-
|
|
393
|
+
# Store project facts
|
|
394
|
+
invt ctx:set db "Postgres 16 on Supabase, migrations in prisma/"
|
|
395
|
+
invt ctx:set api "REST with /api/v2 prefix, auth via JWT middleware"
|
|
396
|
+
invt ctx:set deploy "Fly.io, auto-deploy on push to main"
|
|
397
|
+
|
|
398
|
+
# Record decisions with rationale
|
|
399
|
+
invt ctx:decide auth "JWT in httpOnly cookies" "Chose over localStorage for XSS protection"
|
|
400
|
+
invt ctx:decide orm "Prisma over Drizzle" "Team familiarity, existing migrations"
|
|
401
|
+
|
|
402
|
+
# Query context
|
|
403
|
+
invt ctx:get db
|
|
404
|
+
invt ctx:search auth
|
|
405
|
+
invt ctx:decisions
|
|
406
|
+
|
|
407
|
+
# Dump everything for agent context
|
|
408
|
+
invt ctx:dump
|
|
410
409
|
```
|
|
411
410
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
```typescript
|
|
415
|
-
import { appendFileSync, existsSync, readFileSync } from "fs";
|
|
416
|
-
|
|
417
|
-
class Journal {
|
|
418
|
-
private file = ".journal.md";
|
|
419
|
-
|
|
420
|
-
/** Log a decision or finding */
|
|
421
|
-
async log(c: Context, ...entry: string[]) {
|
|
422
|
-
const ts = new Date().toISOString().slice(0, 16);
|
|
423
|
-
appendFileSync(this.file, `\n## ${ts}\n\n${entry.join(" ")}\n`);
|
|
424
|
-
console.log("Logged.");
|
|
425
|
-
}
|
|
411
|
+
An agent starts a session with `invt ctx:dump` and immediately has structured project knowledge — not flat markdown, not grep results, but queryable facts and decisions with timestamps.
|
|
426
412
|
|
|
427
|
-
|
|
428
|
-
async show(c: Context) {
|
|
429
|
-
if (!existsSync(this.file)) { console.log("No journal yet."); return; }
|
|
430
|
-
console.log(readFileSync(this.file, "utf-8"));
|
|
431
|
-
}
|
|
432
|
-
}
|
|
413
|
+
### Why SQLite?
|
|
433
414
|
|
|
434
|
-
|
|
435
|
-
journal = new Journal();
|
|
436
|
-
}
|
|
437
|
-
```
|
|
415
|
+
Bun bundles SQLite natively — `import { Database } from "bun:sqlite"` just works. No dependencies, no server, no config. The `.ctx.db` file is a single file you can `.gitignore` or commit. You can extend the schema as the project grows: add tables for endpoints, test history, deployment logs, whatever your project needs.
|
|
438
416
|
|
|
439
|
-
|
|
440
|
-
invt journal:log "Chose Postgres over SQLite for concurrent writes"
|
|
441
|
-
invt journal:show
|
|
442
|
-
```
|
|
417
|
+
### Growing the schema
|
|
443
418
|
|
|
444
|
-
|
|
419
|
+
The example above is a starting point. A real project might track more:
|
|
445
420
|
|
|
446
421
|
```typescript
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
export class Tasks {
|
|
466
|
-
search = new Search();
|
|
467
|
-
}
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
```bash
|
|
471
|
-
invt search:code "async.*Context" "*.ts"
|
|
472
|
-
invt search:files "*.test.ts"
|
|
473
|
-
invt search:tree
|
|
422
|
+
// Track API endpoints and their status
|
|
423
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS endpoints (
|
|
424
|
+
path TEXT PRIMARY KEY,
|
|
425
|
+
method TEXT,
|
|
426
|
+
handler TEXT,
|
|
427
|
+
auth TEXT DEFAULT 'required',
|
|
428
|
+
status TEXT DEFAULT 'active'
|
|
429
|
+
)`);
|
|
430
|
+
|
|
431
|
+
// Track test health
|
|
432
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS test_runs (
|
|
433
|
+
id INTEGER PRIMARY KEY,
|
|
434
|
+
suite TEXT,
|
|
435
|
+
passed INTEGER,
|
|
436
|
+
failed INTEGER,
|
|
437
|
+
skipped INTEGER,
|
|
438
|
+
ran_at TEXT DEFAULT (datetime('now'))
|
|
439
|
+
)`);
|
|
474
440
|
```
|
|
475
441
|
|
|
476
|
-
|
|
442
|
+
The namespace class is the interface. The schema is yours to shape around what your project actually needs to remember.
|
|
477
443
|
|
|
478
444
|
## Requirements
|
|
479
445
|
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Context } from "./context";
|
|
3
|
+
import { dirname } from "path";
|
|
3
4
|
import {
|
|
4
5
|
discoverAllTasks,
|
|
5
6
|
discoverRuntimeNamespaces,
|
|
7
|
+
extractImports,
|
|
8
|
+
extractMethodsFromClass,
|
|
9
|
+
findUnresolvedNamespaces,
|
|
6
10
|
parseCommand,
|
|
7
11
|
parseCliArgs,
|
|
8
12
|
resolveArgs,
|
|
@@ -51,6 +55,24 @@ export class Tasks {
|
|
|
51
55
|
// Discover all tasks including namespaced
|
|
52
56
|
const discovered = discoverAllTasks(source);
|
|
53
57
|
|
|
58
|
+
// Resolve imported namespace classes from their source files
|
|
59
|
+
const imports = extractImports(source);
|
|
60
|
+
const unresolved = findUnresolvedNamespaces(source, discovered);
|
|
61
|
+
for (const [propName, className] of unresolved) {
|
|
62
|
+
const importPath = imports.get(className);
|
|
63
|
+
if (!importPath) continue;
|
|
64
|
+
try {
|
|
65
|
+
const resolvedPath = Bun.resolveSync(importPath, dirname(tasksPath));
|
|
66
|
+
const importedSource = await Bun.file(resolvedPath).text();
|
|
67
|
+
const methods = extractMethodsFromClass(importedSource, className);
|
|
68
|
+
if (methods.size > 0) {
|
|
69
|
+
discovered.namespaced.set(propName, methods);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Can't resolve import — runtime discovery will handle it
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
// Also discover imported namespaces from runtime
|
|
55
77
|
discoverRuntimeNamespaces(instance, discovered);
|
|
56
78
|
|
package/src/parser.ts
CHANGED
|
@@ -219,6 +219,47 @@ export function parseParams(
|
|
|
219
219
|
return params;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// Extract import statements: maps imported identifiers to their module paths
|
|
223
|
+
export function extractImports(
|
|
224
|
+
source: string,
|
|
225
|
+
): Map<string, string> {
|
|
226
|
+
const imports = new Map<string, string>();
|
|
227
|
+
const pattern =
|
|
228
|
+
/import\s+(?!\s*type\s)(?:\{([^}]+)\}|(\w+))\s+from\s+["']([^"']+)["']/g;
|
|
229
|
+
let match;
|
|
230
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
231
|
+
const [, namedImports, defaultImport, importPath] = match;
|
|
232
|
+
if (namedImports) {
|
|
233
|
+
for (const spec of namedImports.split(",")) {
|
|
234
|
+
const parts = spec.trim().split(/\s+as\s+/);
|
|
235
|
+
const localName = parts[parts.length - 1].trim();
|
|
236
|
+
if (localName) imports.set(localName, importPath);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (defaultImport) {
|
|
240
|
+
imports.set(defaultImport, importPath);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return imports;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Find namespace assignments whose class wasn't found in the local source
|
|
247
|
+
export function findUnresolvedNamespaces(
|
|
248
|
+
source: string,
|
|
249
|
+
discovered: DiscoveredTasks,
|
|
250
|
+
): Map<string, string> {
|
|
251
|
+
const unresolved = new Map<string, string>(); // propName -> className
|
|
252
|
+
const nsPattern = /(\w+)\s*=\s*new\s+(\w+)\s*\(\s*\)/g;
|
|
253
|
+
let match;
|
|
254
|
+
while ((match = nsPattern.exec(source)) !== null) {
|
|
255
|
+
const [, propName, className] = match;
|
|
256
|
+
if (propName.startsWith("_")) continue;
|
|
257
|
+
if (discovered.namespaced.has(propName)) continue;
|
|
258
|
+
unresolved.set(propName, className);
|
|
259
|
+
}
|
|
260
|
+
return unresolved;
|
|
261
|
+
}
|
|
262
|
+
|
|
222
263
|
// Discover all tasks including namespaced ones (source parsing only)
|
|
223
264
|
export function discoverAllTasks(source: string): DiscoveredTasks {
|
|
224
265
|
const root = extractMethodsFromClass(source, "Tasks");
|