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 CHANGED
@@ -300,180 +300,146 @@ EOF`);
300
300
 
301
301
  ## Agentic Tools
302
302
 
303
- invoket shines as a toolbox for AI agents. Instead of writing ad-hoc scripts each session, the agent adds methods to `tasks.ts` that persist across sessions. `invt --help` shows what tools are available. The file becomes the project's growing command centre.
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
- ### Memorypersist context across sessions
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
- ```typescript
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 { existsSync, readFileSync, writeFileSync } from "fs";
354
-
355
- class Plan {
356
- private file = ".plan.json";
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
- private load(): { task: string; done: boolean }[] {
359
- if (!existsSync(this.file)) return [];
360
- return JSON.parse(readFileSync(this.file, "utf-8"));
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
- private save(tasks: { task: string; done: boolean }[]) {
364
- writeFileSync(this.file, JSON.stringify(tasks, null, 2));
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
- /** Add a step to the plan */
368
- async add(c: Context, ...task: string[]) {
369
- const tasks = this.load();
370
- tasks.push({ task: task.join(" "), done: false });
371
- this.save(tasks);
372
- console.log(`Added step ${tasks.length}: ${task.join(" ")}`);
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
- /** Mark step as done */
376
- async done(c: Context, step: number) {
377
- const tasks = this.load();
378
- tasks[step - 1].done = true;
379
- this.save(tasks);
380
- console.log(`Done: ${tasks[step - 1].task}`);
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
- /** Show the plan */
384
- async show(c: Context) {
385
- const tasks = this.load();
386
- if (!tasks.length) { console.log("No plan yet."); return; }
387
- for (const [i, t] of tasks.entries()) {
388
- console.log(`${t.done ? "✓" : " "} ${i + 1}. ${t.task}`);
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
- /** Clear the plan */
393
- async clear(c: Context) {
394
- this.save([]);
395
- console.log("Plan cleared.");
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
- plan = new Plan();
388
+ ctx = new Ctx();
401
389
  }
402
390
  ```
403
391
 
404
392
  ```bash
405
- invt plan:add "Set up database schema"
406
- invt plan:add "Write API endpoints"
407
- invt plan:add "Add tests"
408
- invt plan:show
409
- invt plan:done 1
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
- ### Session journallog decisions
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
- /** Show recent entries */
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
- export class Tasks {
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
- ```bash
440
- invt journal:log "Chose Postgres over SQLite for concurrent writes"
441
- invt journal:show
442
- ```
417
+ ### Growing the schema
443
418
 
444
- ### Codebase search structured context gathering
419
+ The example above is a starting point. A real project might track more:
445
420
 
446
421
  ```typescript
447
- class Search {
448
- /** Find files matching a pattern */
449
- async files(c: Context, pattern: string) {
450
- await c.run(`find . -name "${pattern}" -not -path "*/node_modules/*"`, { stream: true });
451
- }
452
-
453
- /** Search code for a pattern */
454
- async code(c: Context, pattern: string, ...glob: string[]) {
455
- const g = glob.length ? `--glob '${glob.join("' --glob '")}'` : "";
456
- await c.run(`rg "${pattern}" ${g} --type-not binary`, { stream: true, warn: true });
457
- }
458
-
459
- /** Summarise project structure */
460
- async tree(c: Context) {
461
- await c.run("find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' | head -50", { stream: true });
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
- These tasks persist in the project. Every session, the agent starts with `invt --help` and has its full toolbox ready.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "invoket",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "TypeScript task runner for Bun - uses type annotations to parse CLI arguments",
6
6
  "bin": {
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");