refactor(core): finish TypeScript workspace migration
This commit is contained in:
+1
-1
@@ -31,7 +31,7 @@ promoted to an external tool surface.
|
|||||||
A pure per-source module (claude, codex, later opencode, pi, …) that discovers a
|
A pure per-source module (claude, codex, later opencode, pi, …) that discovers a
|
||||||
source's transcript files and parses one into a stream of records. It never opens
|
source's transcript files and parses one into a stream of records. It never opens
|
||||||
or writes a database; adding a source means adding one adapter. The shared pure
|
or writes a database; adding a source means adding one adapter. The shared pure
|
||||||
parse/discover helpers live in `scripts/parsing.mjs`, which imports only
|
parse/discover helpers live in `packages/core/src/parsing.ts`, which imports only
|
||||||
node:fs/path/os — deliberately node:sqlite-free so the compiled providers can be
|
node:fs/path/os — deliberately node:sqlite-free so the compiled providers can be
|
||||||
consumed by the app (whose Electron runtime has no `node:sqlite`).
|
consumed by the app (whose Electron runtime has no `node:sqlite`).
|
||||||
_Avoid_: parse core, parser, ingest
|
_Avoid_: parse core, parser, ingest
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ The skill directory is provided as `$SKILL_DIR` at invocation time.
|
|||||||
Fast keyword search:
|
Fast keyword search:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node $SKILL_DIR/scripts/runtime.mjs --search "keyword"
|
node $SKILL_DIR/scripts/runtime.js --search "keyword"
|
||||||
```
|
```
|
||||||
|
|
||||||
Custom query:
|
Custom query:
|
||||||
@@ -46,7 +46,7 @@ Custom query:
|
|||||||
2. Run:
|
2. Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node $SKILL_DIR/scripts/runtime.mjs --query /tmp/q.mjs
|
node $SKILL_DIR/scripts/runtime.js --query /tmp/q.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Parse JSON stdout and answer with concise evidence.
|
3. Parse JSON stdout and answer with concise evidence.
|
||||||
@@ -315,7 +315,7 @@ return remember({
|
|||||||
Run the registration script with:
|
Run the registration script with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node $SKILL_DIR/scripts/runtime.mjs --attune /tmp/register-memory.mjs
|
node $SKILL_DIR/scripts/runtime.js --attune /tmp/register-memory.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
`--attune` exposes only memory mutation helpers: `remember()` and `forget()`.
|
`--attune` exposes only memory mutation helpers: `remember()` and `forget()`.
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ function replaceDbWithTemp(tempDbPath, dbPath) {
|
|||||||
function resolveSchemaPath() {
|
function resolveSchemaPath() {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(__dirname, 'schema.sql'),
|
path.join(__dirname, 'schema.sql'),
|
||||||
|
path.join(__dirname, '..', '..', '..', 'packages', 'core', 'src', 'schema.sql'),
|
||||||
path.join(__dirname, '..', 'scripts', 'schema.sql'),
|
path.join(__dirname, '..', 'scripts', 'schema.sql'),
|
||||||
process.resourcesPath ? path.join(process.resourcesPath, 'scripts', 'schema.sql') : null,
|
process.resourcesPath ? path.join(process.resourcesPath, 'scripts', 'schema.sql') : null,
|
||||||
].filter((c): c is string => Boolean(c));
|
].filter((c): c is string => Boolean(c));
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
codexRawId,
|
codexRawId,
|
||||||
codexParentThreadId,
|
codexParentThreadId,
|
||||||
readCodexGuardianThreadInfo,
|
readCodexGuardianThreadInfo,
|
||||||
} from '../../../packages/core/src/parsing.mjs';
|
} from '../../../packages/core/src/parsing.ts';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ interface FileInfo {
|
|||||||
function resolveSchemaPath() {
|
function resolveSchemaPath() {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(__dirname, 'schema.sql'),
|
path.join(__dirname, 'schema.sql'),
|
||||||
path.join(__dirname, '..', '..', '..', 'scripts', 'schema.sql'),
|
path.join(__dirname, '..', '..', '..', 'packages', 'core', 'src', 'schema.sql'),
|
||||||
process.resourcesPath ? path.join(process.resourcesPath, 'scripts', 'schema.sql') : null,
|
process.resourcesPath ? path.join(process.resourcesPath, 'scripts', 'schema.sql') : null,
|
||||||
].filter((c): c is string => Boolean(c));
|
].filter((c): c is string => Boolean(c));
|
||||||
const found = candidates.find(p => fs.existsSync(p));
|
const found = candidates.find(p => fs.existsSync(p));
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
> adapters* (driven by the multi-provider roadmap), and there is *one* shared
|
> adapters* (driven by the multi-provider roadmap), and there is *one* shared
|
||||||
> persist layer, not one per binding.
|
> persist layer, not one per binding.
|
||||||
|
|
||||||
**Context.** Obelisk had two divergent full indexers — `scripts/indexer.mjs`
|
**Context.** Obelisk had two divergent full indexers — the former
|
||||||
(`node:sqlite`, skill/runtime) and `app/indexer.js` (`better-sqlite3`, Electron
|
`scripts/indexer.mjs` (`node:sqlite`, skill/runtime) and `app/indexer.js`
|
||||||
|
(`better-sqlite3`, Electron
|
||||||
app) — that duplicated the same Claude and Codex JSONL parsing and had silently
|
app) — that duplicated the same Claude and Codex JSONL parsing and had silently
|
||||||
diverged in write semantics (`INSERT OR REPLACE` vs `ON CONFLICT DO UPDATE`,
|
diverged in write semantics (`INSERT OR REPLACE` vs `ON CONFLICT DO UPDATE`,
|
||||||
message-count accumulation). Two forces shape the fix: (1) the roadmap will add
|
message-count accumulation). Two forces shape the fix: (1) the roadmap will add
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ the skill artifact must install with **zero build step** on the user's machine
|
|||||||
its checkable contracts, but raises how the compiled output is shipped and which
|
its checkable contracts, but raises how the compiled output is shipped and which
|
||||||
module format it targets.
|
module format it targets.
|
||||||
|
|
||||||
**Decision.** Author all of Core in TypeScript and compile it ahead-of-time to
|
**Decision.** Author all of Core in the `@obelisk/core` npm workspace
|
||||||
|
(`packages/core`) in TypeScript and compile it ahead-of-time to
|
||||||
**ESM JavaScript plus `.d.ts`**. The skill/CLI runtime ships the *precompiled*
|
**ESM JavaScript plus `.d.ts`**. The skill/CLI runtime ships the *precompiled*
|
||||||
ESM JS, so installing the skill never runs a build. Rather than have Core
|
ESM JS, so installing the skill never runs a build. Rather than have Core
|
||||||
dual-publish CJS+ESM, the Electron main process migrates to ESM at Phase 5 so it
|
dual-publish CJS+ESM, the Electron main process migrates to ESM at Phase 5 so it
|
||||||
@@ -20,3 +21,5 @@ in exchange for no dual-build maintenance and a single module format across skil
|
|||||||
CLI, and app. The shipped skill artifact contains compiled JS, not TS. The
|
CLI, and app. The shipped skill artifact contains compiled JS, not TS. The
|
||||||
renderer (Vue) is out of scope and stays JavaScript. Phase 3's TS baseline only
|
renderer (Vue) is out of scope and stays JavaScript. Phase 3's TS baseline only
|
||||||
adds root tooling (package.json, tsconfig, ESLint); it does not touch the app.
|
adds root tooling (package.json, tsconfig, ESLint); it does not touch the app.
|
||||||
|
The app imports Core source so electron-vite can bundle it, while package and
|
||||||
|
skill builds compile the same workspace source to JavaScript.
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ decisions within this:
|
|||||||
security) does not support ESM preload. Source stays ESM; only the preload
|
security) does not support ESM preload. Source stays ESM; only the preload
|
||||||
output format is CJS. `main` loads `../preload/index.js`.
|
output format is CJS. `main` loads `../preload/index.js`.
|
||||||
- **The app consumes the Core from source**: electron-vite/rollup bundles
|
- **The app consumes the Core from source**: electron-vite/rollup bundles
|
||||||
`scripts/providers/*` + `scripts/persist` (and their `scripts/parsing.mjs`
|
`packages/core/src/providers/*` + `packages/core/src/persist.ts` (and their
|
||||||
dependency) into the app's main/worker build, injecting `better-sqlite3`. This
|
`packages/core/src/parsing.ts` dependency) into the app's main/worker build,
|
||||||
|
injecting `better-sqlite3`. This
|
||||||
works because the provider→parsing import graph is node:sqlite-free (ADR-0001),
|
works because the provider→parsing import graph is node:sqlite-free (ADR-0001),
|
||||||
so nothing drags `node:sqlite` into the app. The `dist/` from `build:core`
|
so nothing drags `node:sqlite` into the app. The `dist/` from `build:core`
|
||||||
(ADR-0003) remains for the skill artifact; the app does not need it.
|
(ADR-0003) remains for the skill artifact; the app does not need it.
|
||||||
@@ -30,17 +31,17 @@ decisions within this:
|
|||||||
- **The app main + preload source is TypeScript with types at its seams**, but
|
- **The app main + preload source is TypeScript with types at its seams**, but
|
||||||
under a *deliberately more lenient* project than the runtime core. `app/tsconfig.json`
|
under a *deliberately more lenient* project than the runtime core. `app/tsconfig.json`
|
||||||
keeps `strict` on yet sets `noImplicitAny: false`, because the app mostly
|
keeps `strict` on yet sets `noImplicitAny: false`, because the app mostly
|
||||||
orchestrates the already-strictly-typed core (`scripts/`), and annotating every
|
orchestrates the already-strictly-typed core (`packages/core/src/`), and annotating every
|
||||||
internal SQLite-handle helper would be high-cost, low-value churn. Types are
|
internal SQLite-handle helper would be high-cost, low-value churn. Types are
|
||||||
added where they matter: the core-consumption seam (`BuildIndexOptions`/
|
added where they matter: the core-consumption seam (`BuildIndexOptions`/
|
||||||
`BuildIndexResult`, `FileInfo`), the service/worker factories, and the IPC
|
`BuildIndexResult`, `FileInfo`), the service/worker factories, and the IPC
|
||||||
bridge. Module-to-module specifiers use the real `.ts` extension (mirroring
|
bridge. Module-to-module specifiers use the real `.ts` extension (mirroring
|
||||||
`scripts/`, since Node's type-stripping does not rewrite `.js`→`.ts`), which
|
Core source, since Node's type-stripping does not rewrite `.js`→`.ts`), which
|
||||||
needs `allowImportingTsExtensions` (safe under the project's `noEmit`); the
|
needs `allowImportingTsExtensions` (safe under the project's `noEmit`); the
|
||||||
worker's *runtime* path stays `indexer-worker.js` because that is the built
|
worker's *runtime* path stays `indexer-worker.js` because that is the built
|
||||||
output. `@types/better-sqlite3` is a devDependency for the injected binding.
|
output. `@types/better-sqlite3` is a devDependency for the injected binding.
|
||||||
|
|
||||||
**Two-tier typechecking.** `npm run typecheck` runs the root project (`scripts/` +
|
**Two-tier typechecking.** `npm run typecheck` runs the root project (`packages/core/src/` +
|
||||||
`tests/`, fully strict including `noImplicitAny`) and then the app project. The
|
`tests/`, fully strict including `noImplicitAny`) and then the app project. The
|
||||||
root project **excludes the app-importing tests** (`tests/app-*.test.mjs`,
|
root project **excludes the app-importing tests** (`tests/app-*.test.mjs`,
|
||||||
`tests/recap-capture-query.test.mjs`): those tests import app source, which would
|
`tests/recap-capture-query.test.mjs`): those tests import app source, which would
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ failed statement can replay part of a transaction.
|
|||||||
**Decision.** Use one transaction primitive plus two explicit coordination
|
**Decision.** Use one transaction primitive plus two explicit coordination
|
||||||
layers.
|
layers.
|
||||||
|
|
||||||
- `scripts/tx.ts` owns the binding-agnostic `runWriteTransaction(db, work)`.
|
- `packages/core/src/tx.ts` owns the binding-agnostic
|
||||||
|
`runWriteTransaction(db, work)`.
|
||||||
Adapters expose transaction state from better-sqlite3's `inTransaction` and
|
Adapters expose transaction state from better-sqlite3's `inTransaction` and
|
||||||
node:sqlite's `isTransaction`. The primitive performs `BEGIN IMMEDIATE`, runs
|
node:sqlite's `isTransaction`. The primitive performs `BEGIN IMMEDIATE`, runs
|
||||||
`work` exactly once, commits, and attempts rollback only when the binding says
|
`work` exactly once, commits, and attempts rollback only when the binding says
|
||||||
a transaction is active or its state is unknown. Cleanup never masks the
|
a transaction is active or its state is unknown. Cleanup never masks the
|
||||||
primary exception. Diagnostics record phase, SQLite code, rollback outcome,
|
primary exception. Diagnostics record phase, SQLite code, rollback outcome,
|
||||||
transaction state, label, and attempts.
|
transaction state, label, and attempts.
|
||||||
- Retry is an upper-layer policy in `scripts/write-coordinator.ts`, never hidden
|
- Retry is an upper-layer policy in `packages/core/src/write-coordinator.ts`, never hidden
|
||||||
inside the transaction primitive. Only an idempotent whole transaction that
|
inside the transaction primitive. Only an idempotent whole transaction that
|
||||||
failed during work/commit with `SQLITE_BUSY*` and is confirmed inactive may be
|
failed during work/commit with `SQLITE_BUSY*` and is confirmed inactive may be
|
||||||
retried. The default is three attempts within a one-second budget with short
|
retried. The default is three attempts within a one-second budget with short
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// Flat ESLint config for the Obelisk root (Core + skill runtime + tests).
|
// Flat ESLint config for the Obelisk root (Core + skill runtime + tests).
|
||||||
// Scope: the ESM/TS sources under scripts/ and tests/. The Electron app has its
|
// Scope: the ESM/TS sources under packages/core/src/ and tests/. The Electron app has its
|
||||||
// own package and toolchain and is intentionally excluded (see docs/adr/0003).
|
// own package and toolchain and is intentionally excluded (see docs/adr/0003).
|
||||||
|
|
||||||
import js from '@eslint/js';
|
import js from '@eslint/js';
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
// Obelisk Core package (see docs/adr/0003-core-typescript-esm-precompiled.md).
|
// Obelisk Core package (see docs/adr/0003-core-typescript-esm-precompiled.md).
|
||||||
//
|
//
|
||||||
// The single shared implementation behind every transport. runtime.mjs (skill),
|
// The single shared implementation behind every transport. runtime.js (skill),
|
||||||
// and later the CLI and MCP server, are thin shells over these four functions;
|
// and later the CLI and MCP server, are thin shells over these four functions;
|
||||||
// none of them re-implement retrieval or own the DB lifecycle.
|
// none of them re-implement retrieval or own the DB lifecycle.
|
||||||
//
|
//
|
||||||
// Authored in TypeScript with erasable-only syntax so Node can run it directly
|
// Authored in TypeScript with erasable-only syntax so Node can run it directly
|
||||||
// via type stripping in development, while the skill artifact ships the tsc
|
// via type stripping in development, while the skill artifact ships readable,
|
||||||
// output (Phase 6). The heavy internals (db/indexer/query) remain .mjs for now
|
// non-bundled tsc output. Core source lives in the @obelisk/core workspace.
|
||||||
// and are migrated in later phases; Core is the typed seam over them.
|
|
||||||
|
|
||||||
import { createContext, runInNewContext } from 'node:vm';
|
import { createContext, runInNewContext } from 'node:vm';
|
||||||
|
|
||||||
import { DB_PATH, openDb, openReadDb, openWriterLeaseDb } from './db.mjs';
|
import { DB_PATH, openDb, openReadDb, openWriterLeaseDb } from './db.ts';
|
||||||
import { buildIndex, shouldSkipBuild } from './indexer.mjs';
|
import { buildIndex, shouldSkipBuild } from './indexer.ts';
|
||||||
import { createQueryApi, createAttuneApi } from './query.mjs';
|
import { createQueryApi, createAttuneApi } from './query.ts';
|
||||||
import { acquireWriterLease, writerLockPathFor } from './writer-lease.ts';
|
import { acquireWriterLease, writerLockPathFor } from './writer-lease.ts';
|
||||||
|
|
||||||
export { buildIndex, DB_PATH };
|
export { buildIndex, DB_PATH };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// node:sqlite lifecycle and migrations for the Core package.
|
// node:sqlite lifecycle and migrations for the Core package.
|
||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
import { CLAUDE_DIR, CODEX_DIR, TEXT_LIMIT, trunc, truncJson, extractText, extractContentType, extractMessageIsMeta, filePath, isDir, readLines } from './parsing.mjs';
|
import { CLAUDE_DIR, CODEX_DIR, TEXT_LIMIT, trunc, truncJson, extractText, extractContentType, extractMessageIsMeta, filePath, isDir, readLines } from './parsing.ts';
|
||||||
import { configureConnection } from './tx.ts';
|
import { configureConnection } from './tx.ts';
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
@@ -8,6 +8,8 @@ const path = require('node:path');
|
|||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const { DatabaseSync } = require('node:sqlite');
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
|
||||||
|
type SqliteDb = any;
|
||||||
|
|
||||||
const OBELISK_DIR = path.join(os.homedir(), '.obelisk');
|
const OBELISK_DIR = path.join(os.homedir(), '.obelisk');
|
||||||
const LEGACY_DB_PATH = path.join(CLAUDE_DIR, 'obelisk.sqlite');
|
const LEGACY_DB_PATH = path.join(CLAUDE_DIR, 'obelisk.sqlite');
|
||||||
const DB_PATH = path.join(OBELISK_DIR, 'obelisk.sqlite');
|
const DB_PATH = path.join(OBELISK_DIR, 'obelisk.sqlite');
|
||||||
@@ -39,20 +41,20 @@ function openReadDb() {
|
|||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openWriterLeaseDb(lockPath) {
|
function openWriterLeaseDb(lockPath: string): SqliteDb {
|
||||||
return new DatabaseSync(lockPath);
|
return new DatabaseSync(lockPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureColumn(db, table, column, definition) {
|
function ensureColumn(db: SqliteDb, table: string, column: string, definition: string): void {
|
||||||
const columns = db.prepare(`PRAGMA table_info(${table})`).all().map(c => c.name);
|
const columns = db.prepare(`PRAGMA table_info(${table})`).all().map((c: { name: string }) => c.name);
|
||||||
if (!columns.includes(column)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
if (!columns.includes(column)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tableExists(db, table) {
|
function tableExists(db: SqliteDb, table: string): boolean {
|
||||||
return Boolean(db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
return Boolean(db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateExistingColumns(db) {
|
function migrateExistingColumns(db: SqliteDb): void {
|
||||||
if (tableExists(db, 'sessions')) ensureColumn(db, 'sessions', 'source', "TEXT DEFAULT 'claude'");
|
if (tableExists(db, 'sessions')) ensureColumn(db, 'sessions', 'source', "TEXT DEFAULT 'claude'");
|
||||||
if (tableExists(db, 'messages')) {
|
if (tableExists(db, 'messages')) {
|
||||||
ensureColumn(db, 'messages', 'content_type', 'TEXT');
|
ensureColumn(db, 'messages', 'content_type', 'TEXT');
|
||||||
@@ -66,11 +68,11 @@ function migrateExistingColumns(db) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateDb(db) {
|
function migrateDb(db: SqliteDb): void {
|
||||||
migrateExistingColumns(db);
|
migrateExistingColumns(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildMemoryFts(db) {
|
function rebuildMemoryFts(db: SqliteDb): void {
|
||||||
db.exec("INSERT INTO memories_fts(memories_fts) VALUES('rebuild')");
|
db.exec("INSERT INTO memories_fts(memories_fts) VALUES('rebuild')");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,20 +1,48 @@
|
|||||||
// Passive-pull indexing orchestration for the Core package.
|
// Passive-pull indexing orchestration for the Core package.
|
||||||
import { DB_PATH, openDb, openReadDb, openWriterLeaseDb, rebuildMemoryFts } from './db.mjs';
|
import { DB_PATH, openDb, openReadDb, openWriterLeaseDb, rebuildMemoryFts } from './db.ts';
|
||||||
import {
|
import {
|
||||||
CLAUDE_DIR, CODEX_DIR, PROJECTS_DIR, fs, path, isDir, readLines,
|
CLAUDE_DIR, CODEX_DIR, PROJECTS_DIR, fs, path, isDir, readLines,
|
||||||
inferProjectPath, discoverJsonlFiles, discoverCodexJsonlFiles, codexDbId, readCodexGuardianThreadInfo,
|
inferProjectPath, discoverJsonlFiles, discoverCodexJsonlFiles, codexDbId, readCodexGuardianThreadInfo,
|
||||||
} from './parsing.mjs';
|
} from './parsing.ts';
|
||||||
import { persist } from './persist.ts';
|
import { persist } from './persist.ts';
|
||||||
import { nodeSqliteTransactionAdapter } from './tx.ts';
|
import { nodeSqliteTransactionAdapter } from './tx.ts';
|
||||||
import { acquireWriterLease, writerLockPathFor } from './writer-lease.ts';
|
import { acquireWriterLease, writerLockPathFor } from './writer-lease.ts';
|
||||||
import { runRetryableWriteTransaction, isBeginBusyFailure, hasUnusableTransaction } from './write-coordinator.ts';
|
import { runRetryableWriteTransaction, isBeginBusyFailure, hasUnusableTransaction } from './write-coordinator.ts';
|
||||||
import { parse as claudeParse } from './providers/claude.ts';
|
import { parse as claudeParse } from './providers/claude.ts';
|
||||||
import { parse as codexParse } from './providers/codex.ts';
|
import { parse as codexParse } from './providers/codex.ts';
|
||||||
|
import type { Cursor, IndexRecord } from './providers/types.ts';
|
||||||
|
|
||||||
const HISTORY_PATH = path.join(CLAUDE_DIR, 'history.jsonl');
|
const HISTORY_PATH = path.join(CLAUDE_DIR, 'history.jsonl');
|
||||||
|
|
||||||
|
type SqliteDb = any;
|
||||||
|
type JsonRecord = Record<string, any>;
|
||||||
|
|
||||||
function needsReindex(db, fp) {
|
interface ClaudeFileInfo {
|
||||||
|
path: string;
|
||||||
|
sessionId: string;
|
||||||
|
project: string;
|
||||||
|
isSubagent: boolean;
|
||||||
|
agentId?: string;
|
||||||
|
workflowRunId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SkippedFile {
|
||||||
|
path: string;
|
||||||
|
error: string;
|
||||||
|
diagnostics?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BuildCheckOptions {
|
||||||
|
now?: number;
|
||||||
|
ignoreRecentBuild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorMessage(error: unknown): string {
|
||||||
|
return error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function needsReindex(db: SqliteDb, fp: string) {
|
||||||
const mt = fs.statSync(fp).mtimeMs;
|
const mt = fs.statSync(fp).mtimeMs;
|
||||||
const row = db.prepare('SELECT mtime, lines_processed FROM index_state WHERE jsonl_path = ?').get(fp);
|
const row = db.prepare('SELECT mtime, lines_processed FROM index_state WHERE jsonl_path = ?').get(fp);
|
||||||
if (!row) return { needed: true, skip: 0 };
|
if (!row) return { needed: true, skip: 0 };
|
||||||
@@ -22,15 +50,15 @@ function needsReindex(db, fp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function indexCodexSessionIndex(db) {
|
function indexCodexSessionIndex(db: SqliteDb): void {
|
||||||
const indexPath = path.join(CODEX_DIR, 'session_index.jsonl');
|
const indexPath = path.join(CODEX_DIR, 'session_index.jsonl');
|
||||||
if (!fs.existsSync(indexPath)) return;
|
if (!fs.existsSync(indexPath)) return;
|
||||||
readLines(indexPath, (line) => {
|
readLines(indexPath, (line) => {
|
||||||
let item;
|
let item: JsonRecord;
|
||||||
try {
|
try {
|
||||||
item = JSON.parse(line);
|
item = JSON.parse(line);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
process.stderr.write(`Warning: malformed Codex session index line: ${e.message}\n`);
|
process.stderr.write(`Warning: malformed Codex session index line: ${errorMessage(e)}\n`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!item.id || !item.thread_name) return;
|
if (!item.id || !item.thread_name) return;
|
||||||
@@ -39,7 +67,7 @@ function indexCodexSessionIndex(db) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshSessionProjectPaths(db) {
|
function refreshSessionProjectPaths(db: SqliteDb): void {
|
||||||
const sessions = db.prepare('SELECT id, project FROM sessions').all();
|
const sessions = db.prepare('SELECT id, project FROM sessions').all();
|
||||||
const cwdStmt = db.prepare(`
|
const cwdStmt = db.prepare(`
|
||||||
SELECT cwd
|
SELECT cwd
|
||||||
@@ -49,21 +77,21 @@ function refreshSessionProjectPaths(db) {
|
|||||||
`);
|
`);
|
||||||
const update = db.prepare('UPDATE sessions SET project_path = ? WHERE id = ?');
|
const update = db.prepare('UPDATE sessions SET project_path = ? WHERE id = ?');
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
const cwds = cwdStmt.all(session.id).map(row => row.cwd);
|
const cwds = cwdStmt.all(session.id).map((row: JsonRecord) => row.cwd);
|
||||||
const projectPath = inferProjectPath(session.project, cwds);
|
const projectPath = inferProjectPath(session.project, cwds);
|
||||||
if (projectPath) update.run(projectPath, session.id);
|
if (projectPath) update.run(projectPath, session.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function indexSubagentMeta(db, fi) {
|
function indexSubagentMeta(db: SqliteDb, fi: ClaudeFileInfo): void {
|
||||||
if (!fi.isSubagent) return;
|
if (!fi.isSubagent) return;
|
||||||
const mp = fi.path.replace('.jsonl', '.meta.json');
|
const mp = fi.path.replace('.jsonl', '.meta.json');
|
||||||
if (!fs.existsSync(mp)) return;
|
if (!fs.existsSync(mp)) return;
|
||||||
let meta;
|
let meta: JsonRecord;
|
||||||
try {
|
try {
|
||||||
meta = JSON.parse(fs.readFileSync(mp, 'utf8'));
|
meta = JSON.parse(fs.readFileSync(mp, 'utf8'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
process.stderr.write(`Warning: failed to read subagent meta ${mp}: ${e.message}\n`);
|
process.stderr.write(`Warning: failed to read subagent meta ${mp}: ${errorMessage(e)}\n`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tok = db.prepare('SELECT COALESCE(SUM(input_tokens),0)+COALESCE(SUM(output_tokens),0) as t FROM messages WHERE agent_id=?').get(fi.agentId);
|
const tok = db.prepare('SELECT COALESCE(SUM(input_tokens),0)+COALESCE(SUM(output_tokens),0) as t FROM messages WHERE agent_id=?').get(fi.agentId);
|
||||||
@@ -76,7 +104,7 @@ function indexSubagentMeta(db, fi) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function indexWorkflows(db) {
|
function indexWorkflows(db: SqliteDb): void {
|
||||||
if (!fs.existsSync(PROJECTS_DIR)) return;
|
if (!fs.existsSync(PROJECTS_DIR)) return;
|
||||||
let projects;
|
let projects;
|
||||||
try { projects = fs.readdirSync(PROJECTS_DIR); } catch { return; }
|
try { projects = fs.readdirSync(PROJECTS_DIR); } catch { return; }
|
||||||
@@ -92,11 +120,11 @@ function indexWorkflows(db) {
|
|||||||
try { wfFiles = fs.readdirSync(wd); } catch { continue; }
|
try { wfFiles = fs.readdirSync(wd); } catch { continue; }
|
||||||
for (const f of wfFiles) {
|
for (const f of wfFiles) {
|
||||||
if (!f.endsWith('.json')) continue;
|
if (!f.endsWith('.json')) continue;
|
||||||
let wf;
|
let wf: JsonRecord;
|
||||||
try {
|
try {
|
||||||
wf = JSON.parse(fs.readFileSync(path.join(wd, f), 'utf8'));
|
wf = JSON.parse(fs.readFileSync(path.join(wd, f), 'utf8'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
process.stderr.write(`Warning: failed to read workflow ${f}: ${e.message}\n`);
|
process.stderr.write(`Warning: failed to read workflow ${f}: ${errorMessage(e)}\n`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!wf.runId) continue;
|
if (!wf.runId) continue;
|
||||||
@@ -117,14 +145,14 @@ function indexWorkflows(db) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function indexHistory(db) {
|
function indexHistory(db: SqliteDb): void {
|
||||||
if (!fs.existsSync(HISTORY_PATH)) return;
|
if (!fs.existsSync(HISTORY_PATH)) return;
|
||||||
readLines(HISTORY_PATH, (line) => {
|
readLines(HISTORY_PATH, (line) => {
|
||||||
let item;
|
let item: JsonRecord;
|
||||||
try {
|
try {
|
||||||
item = JSON.parse(line);
|
item = JSON.parse(line);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
process.stderr.write(`Warning: malformed history line: ${e.message}\n`);
|
process.stderr.write(`Warning: malformed history line: ${errorMessage(e)}\n`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (item.sessionId && item.title) db.prepare('UPDATE sessions SET title=? WHERE id=? AND title IS NULL').run(item.title, item.sessionId);
|
if (item.sessionId && item.title) db.prepare('UPDATE sessions SET title=? WHERE id=? AND title IS NULL').run(item.title, item.sessionId);
|
||||||
@@ -134,7 +162,7 @@ function indexHistory(db) {
|
|||||||
const BUILD_DEBOUNCE_MS = 30000;
|
const BUILD_DEBOUNCE_MS = 30000;
|
||||||
const APP_HEARTBEAT_FRESH_MS = 60000;
|
const APP_HEARTBEAT_FRESH_MS = 60000;
|
||||||
|
|
||||||
function shouldSkipBuild(db, { now = Date.now(), ignoreRecentBuild = false } = {}) {
|
function shouldSkipBuild(db: SqliteDb, { now = Date.now(), ignoreRecentBuild = false }: BuildCheckOptions = {}) {
|
||||||
const appHeartbeat = db.prepare("SELECT mtime FROM index_state WHERE jsonl_path='__app_heartbeat__'").get();
|
const appHeartbeat = db.prepare("SELECT mtime FROM index_state WHERE jsonl_path='__app_heartbeat__'").get();
|
||||||
if (appHeartbeat && now - appHeartbeat.mtime < APP_HEARTBEAT_FRESH_MS) {
|
if (appHeartbeat && now - appHeartbeat.mtime < APP_HEARTBEAT_FRESH_MS) {
|
||||||
return { skip: true, reason: 'daemon_active' };
|
return { skip: true, reason: 'daemon_active' };
|
||||||
@@ -148,12 +176,12 @@ function shouldSkipBuild(db, { now = Date.now(), ignoreRecentBuild = false } = {
|
|||||||
return { skip: false };
|
return { skip: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMissingIndexStateTable(error) {
|
function isMissingIndexStateTable(error: unknown): boolean {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
return /no such table:\s*(?:main\.)?index_state\b/i.test(message);
|
return /no such table:\s*(?:main\.)?index_state\b/i.test(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inspectBuildOwnership({ force = false } = {}) {
|
function inspectBuildOwnership({ force = false }: { force?: boolean } = {}) {
|
||||||
if (!fs.existsSync(DB_PATH)) return { skip: false };
|
if (!fs.existsSync(DB_PATH)) return { skip: false };
|
||||||
const db = openReadDb();
|
const db = openReadDb();
|
||||||
try {
|
try {
|
||||||
@@ -170,12 +198,12 @@ function inspectBuildOwnership({ force = false } = {}) {
|
|||||||
|
|
||||||
// A one-shot record stream that retracts a session, for routing guardian sweeps
|
// A one-shot record stream that retracts a session, for routing guardian sweeps
|
||||||
// through persist (the single db writer) instead of deleting rows directly.
|
// through persist (the single db writer) instead of deleting rows directly.
|
||||||
function* guardianDelete(sessionId) {
|
function* guardianDelete(sessionId: string): Generator<IndexRecord, Cursor> {
|
||||||
yield { kind: 'delete-session', sessionId };
|
yield { kind: 'delete-session', sessionId };
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildIndex({ force = false } = {}) {
|
function buildIndex({ force = false }: { force?: boolean } = {}) {
|
||||||
const ownership = inspectBuildOwnership({ force });
|
const ownership = inspectBuildOwnership({ force });
|
||||||
if (ownership.skip) return ownership;
|
if (ownership.skip) return ownership;
|
||||||
const lease = acquireWriterLease({
|
const lease = acquireWriterLease({
|
||||||
@@ -190,7 +218,7 @@ function buildIndex({ force = false } = {}) {
|
|||||||
|
|
||||||
const db = openDb();
|
const db = openDb();
|
||||||
const txDb = nodeSqliteTransactionAdapter(db);
|
const txDb = nodeSqliteTransactionAdapter(db);
|
||||||
const skippedFiles = [];
|
const skippedFiles: SkippedFile[] = [];
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
if (force) {
|
if (force) {
|
||||||
@@ -232,7 +260,8 @@ function buildIndex({ force = false } = {}) {
|
|||||||
} else {
|
} else {
|
||||||
const guardian = readCodexGuardianThreadInfo(f.path);
|
const guardian = readCodexGuardianThreadInfo(f.path);
|
||||||
if (guardian) {
|
if (guardian) {
|
||||||
persist(db, { key: f.path, sessionId: '' }, guardianDelete(codexDbId(guardian.threadRawId)));
|
const sessionId = codexDbId(guardian.threadRawId);
|
||||||
|
if (sessionId) persist(db, { key: f.path, sessionId: '' }, guardianDelete(sessionId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -253,8 +282,10 @@ function buildIndex({ force = false } = {}) {
|
|||||||
}
|
}
|
||||||
if (hasUnusableTransaction(e)) throw e;
|
if (hasUnusableTransaction(e)) throw e;
|
||||||
// A per-file failure is skippable: log and move on.
|
// A per-file failure is skippable: log and move on.
|
||||||
skippedFiles.push({ path: f.path, error: e.message, diagnostics: e.obelisk });
|
const error = e as { message?: unknown; obelisk?: unknown } | null;
|
||||||
process.stderr.write(`Warning: failed to index ${f.path}: ${e.message}\n`);
|
const message = errorMessage(e);
|
||||||
|
skippedFiles.push({ path: f.path, error: message, diagnostics: error?.obelisk });
|
||||||
|
process.stderr.write(`Warning: failed to index ${f.path}: ${message}\n`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Finalize is one transaction and is NOT swallowed: a finalize failure fails
|
// Finalize is one transaction and is NOT swallowed: a finalize failure fails
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Core's pure parse/discover helpers — node:sqlite-free by construction, so the compiled
|
// Core's pure parse/discover helpers — node:sqlite-free by construction, so the compiled
|
||||||
// providers can be consumed by the app (better-sqlite3 / a Node without
|
// providers can be consumed by the app (better-sqlite3 / a Node without
|
||||||
// node:sqlite). Moved verbatim from db.mjs and indexer.mjs (Phase 5d-1); they use
|
// node:sqlite). Originally extracted verbatim from db/indexer; it now exposes a
|
||||||
// only node:fs/path/os. Kept as .mjs (plain ESM) for a low-risk verbatim move.
|
// typed seam while remaining limited to node:fs/path/os.
|
||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
@@ -14,18 +14,41 @@ const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|||||||
const CODEX_SESSIONS_DIR = path.join(CODEX_DIR, 'sessions');
|
const CODEX_SESSIONS_DIR = path.join(CODEX_DIR, 'sessions');
|
||||||
const TEXT_LIMIT = 10000;
|
const TEXT_LIMIT = 10000;
|
||||||
|
|
||||||
// ---- from db.mjs (message/text helpers) ----
|
type JsonRecord = Record<string, any>;
|
||||||
function trunc(s) {
|
type JsonValue = any;
|
||||||
|
|
||||||
|
interface ClaudeJsonlFile {
|
||||||
|
path: string;
|
||||||
|
sessionId: string;
|
||||||
|
project: string;
|
||||||
|
isSubagent: boolean;
|
||||||
|
agentId?: string;
|
||||||
|
workflowRunId?: string;
|
||||||
|
source?: 'claude';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodexJsonlFile {
|
||||||
|
path: string;
|
||||||
|
source: 'codex';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodexLineRecord {
|
||||||
|
lineNum: number;
|
||||||
|
obj: JsonRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- message/text helpers ----
|
||||||
|
function trunc(s: any): any {
|
||||||
return typeof s === 'string' && s.length > TEXT_LIMIT ? s.slice(0, TEXT_LIMIT) : s;
|
return typeof s === 'string' && s.length > TEXT_LIMIT ? s.slice(0, TEXT_LIMIT) : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncJson(obj, limit = TEXT_LIMIT) {
|
function truncJson(obj: JsonValue, limit = TEXT_LIMIT): string | null {
|
||||||
if (obj === null || obj === undefined) return null;
|
if (obj === null || obj === undefined) return null;
|
||||||
const walk = (v) => {
|
const walk = (v: JsonValue): JsonValue => {
|
||||||
if (typeof v === 'string') return v.length > limit ? v.slice(0, limit) + '...[truncated]' : v;
|
if (typeof v === 'string') return v.length > limit ? v.slice(0, limit) + '...[truncated]' : v;
|
||||||
if (Array.isArray(v)) return v.map(walk);
|
if (Array.isArray(v)) return v.map(walk);
|
||||||
if (typeof v === 'object' && v !== null) {
|
if (typeof v === 'object' && v !== null) {
|
||||||
const out = {};
|
const out: JsonRecord = {};
|
||||||
for (const [k, val] of Object.entries(v)) out[k] = walk(val);
|
for (const [k, val] of Object.entries(v)) out[k] = walk(val);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -34,10 +57,10 @@ function truncJson(obj, limit = TEXT_LIMIT) {
|
|||||||
return JSON.stringify(walk(obj));
|
return JSON.stringify(walk(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractText(content) {
|
function extractText(content: JsonValue): string | null {
|
||||||
if (typeof content === 'string') return trunc(content);
|
if (typeof content === 'string') return trunc(content);
|
||||||
if (!Array.isArray(content)) return null;
|
if (!Array.isArray(content)) return null;
|
||||||
const parts = [];
|
const parts: string[] = [];
|
||||||
for (const b of content) {
|
for (const b of content) {
|
||||||
if (b.type === 'text' && b.text) parts.push(b.text);
|
if (b.type === 'text' && b.text) parts.push(b.text);
|
||||||
else if (b.type === 'thinking' && b.thinking) parts.push(b.thinking);
|
else if (b.type === 'thinking' && b.thinking) parts.push(b.thinking);
|
||||||
@@ -45,10 +68,10 @@ function extractText(content) {
|
|||||||
return parts.length ? trunc(parts.join('\n')) : null;
|
return parts.length ? trunc(parts.join('\n')) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractContentType(content) {
|
function extractContentType(content: JsonValue): string {
|
||||||
if (typeof content === 'string') return 'text';
|
if (typeof content === 'string') return 'text';
|
||||||
if (!Array.isArray(content) || !content.length) return 'unknown';
|
if (!Array.isArray(content) || !content.length) return 'unknown';
|
||||||
const types = new Set();
|
const types = new Set<string>();
|
||||||
let sawUnknown = false;
|
let sawUnknown = false;
|
||||||
for (const b of content) {
|
for (const b of content) {
|
||||||
if (!b || typeof b !== 'object') { sawUnknown = true; continue; }
|
if (!b || typeof b !== 'object') { sawUnknown = true; continue; }
|
||||||
@@ -63,20 +86,20 @@ function extractContentType(content) {
|
|||||||
|
|
||||||
const COMMAND_ENVELOPE_RE = /^\s*(<command-name>[^<]+<\/command-name>|<(?:task-notification|system-reminder)\b|<local-command(?:\b|-))/;
|
const COMMAND_ENVELOPE_RE = /^\s*(<command-name>[^<]+<\/command-name>|<(?:task-notification|system-reminder)\b|<local-command(?:\b|-))/;
|
||||||
|
|
||||||
function extractMessageIsMeta(record, text = extractText(record?.message?.content)) {
|
function extractMessageIsMeta(record: JsonRecord, text: string | null = extractText(record?.message?.content)): 0 | 1 {
|
||||||
const msg = record?.message || {};
|
const msg = record?.message || {};
|
||||||
if (record?.isMeta === true || msg.isMeta === true) return 1;
|
if (record?.isMeta === true || msg.isMeta === true) return 1;
|
||||||
return typeof text === 'string' && COMMAND_ENVELOPE_RE.test(text) ? 1 : 0;
|
return typeof text === 'string' && COMMAND_ENVELOPE_RE.test(text) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filePath(name, input) {
|
function filePath(name: string, input: JsonRecord | null | undefined): string | null {
|
||||||
if (!input) return null;
|
if (!input) return null;
|
||||||
return ['Read', 'Edit', 'Write', 'NotebookEdit'].includes(name) ? (input.file_path || null) : null;
|
return ['Read', 'Edit', 'Write', 'NotebookEdit'].includes(name) ? (input.file_path || null) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
|
function isDir(p: string): boolean { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
|
||||||
|
|
||||||
function readLines(filePath, callback) {
|
function readLines(filePath: string, callback: (line: string) => boolean | void): void {
|
||||||
const fd = fs.openSync(filePath, 'r');
|
const fd = fs.openSync(filePath, 'r');
|
||||||
const bufSize = 64 * 1024;
|
const bufSize = 64 * 1024;
|
||||||
const buf = Buffer.alloc(bufSize);
|
const buf = Buffer.alloc(bufSize);
|
||||||
@@ -86,7 +109,7 @@ function readLines(filePath, callback) {
|
|||||||
while ((bytesRead = fs.readSync(fd, buf, 0, bufSize)) > 0) {
|
while ((bytesRead = fs.readSync(fd, buf, 0, bufSize)) > 0) {
|
||||||
const chunk = remainder + buf.toString('utf8', 0, bytesRead);
|
const chunk = remainder + buf.toString('utf8', 0, bytesRead);
|
||||||
const lines = chunk.split('\n');
|
const lines = chunk.split('\n');
|
||||||
remainder = lines.pop();
|
remainder = lines.pop() ?? '';
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line && callback(line) === false) return;
|
if (line && callback(line) === false) return;
|
||||||
}
|
}
|
||||||
@@ -97,25 +120,25 @@ function readLines(filePath, callback) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- from indexer.mjs (project-path + discovery helpers) ----
|
// ---- project-path + discovery helpers ----
|
||||||
function legacyProjectPathFromSlug(project) {
|
function legacyProjectPathFromSlug(project: string | null | undefined): string | null {
|
||||||
if (!project) return null;
|
if (!project) return null;
|
||||||
return '/' + project.replace(/-/g, '/').replace(/^\//, '');
|
return '/' + project.replace(/-/g, '/').replace(/^\//, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeObservedCwd(cwd) {
|
function normalizeObservedCwd(cwd: unknown): string | null {
|
||||||
if (typeof cwd !== 'string' || !cwd.trim() || !path.isAbsolute(cwd)) return null;
|
if (typeof cwd !== 'string' || !cwd.trim() || !path.isAbsolute(cwd)) return null;
|
||||||
return path.normalize(cwd);
|
return path.normalize(cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectSlugFromPath(projectPath) {
|
function projectSlugFromPath(projectPath: string | null): string | null {
|
||||||
const normalized = normalizeObservedCwd(projectPath);
|
const normalized = normalizeObservedCwd(projectPath);
|
||||||
if (!normalized) return null;
|
if (!normalized) return null;
|
||||||
return '-' + normalized.replace(/^[\\/]+/, '').replace(/[\\/]+/g, '-');
|
return '-' + normalized.replace(/^[\\/]+/, '').replace(/[\\/]+/g, '-');
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferProjectPath(project, observedCwds = []) {
|
function inferProjectPath(project: string | null | undefined, observedCwds: unknown[] = []): string | null {
|
||||||
const byPath = new Map();
|
const byPath = new Map<string, { path: string; count: number; first: number }>();
|
||||||
for (const cwd of observedCwds) {
|
for (const cwd of observedCwds) {
|
||||||
const normalized = normalizeObservedCwd(cwd);
|
const normalized = normalizeObservedCwd(cwd);
|
||||||
if (!normalized) continue;
|
if (!normalized) continue;
|
||||||
@@ -127,11 +150,11 @@ function inferProjectPath(project, observedCwds = []) {
|
|||||||
return best?.path || legacyProjectPathFromSlug(project);
|
return best?.path || legacyProjectPathFromSlug(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
function discoverJsonlFiles() {
|
function discoverJsonlFiles(): ClaudeJsonlFile[] {
|
||||||
const files = [];
|
const files: ClaudeJsonlFile[] = [];
|
||||||
if (!fs.existsSync(PROJECTS_DIR)) return files;
|
if (!fs.existsSync(PROJECTS_DIR)) return files;
|
||||||
let projects;
|
let projects;
|
||||||
try { projects = fs.readdirSync(PROJECTS_DIR); } catch (e) { process.stderr.write(`Warning: cannot read projects dir: ${e.message}\n`); return files; }
|
try { projects = fs.readdirSync(PROJECTS_DIR); } catch (e) { process.stderr.write(`Warning: cannot read projects dir: ${e instanceof Error ? e.message : String(e)}\n`); return files; }
|
||||||
for (const proj of projects) {
|
for (const proj of projects) {
|
||||||
const projPath = path.join(PROJECTS_DIR, proj);
|
const projPath = path.join(PROJECTS_DIR, proj);
|
||||||
if (!isDir(projPath)) continue;
|
if (!isDir(projPath)) continue;
|
||||||
@@ -169,10 +192,10 @@ function discoverJsonlFiles() {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
function discoverCodexJsonlFiles() {
|
function discoverCodexJsonlFiles(): CodexJsonlFile[] {
|
||||||
const files = [];
|
const files: CodexJsonlFile[] = [];
|
||||||
if (!fs.existsSync(CODEX_SESSIONS_DIR)) return files;
|
if (!fs.existsSync(CODEX_SESSIONS_DIR)) return files;
|
||||||
const walk = (dir) => {
|
const walk = (dir: string): void => {
|
||||||
let entries;
|
let entries;
|
||||||
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -188,27 +211,27 @@ function discoverCodexJsonlFiles() {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- from indexer.mjs (codex pure helpers) ----
|
// ---- Codex pure helpers ----
|
||||||
function codexDbId(id) {
|
function codexDbId(id: unknown): string | null {
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
const raw = String(id).replace(/^codex:/, '');
|
const raw = String(id).replace(/^codex:/, '');
|
||||||
return `codex:${raw}`;
|
return `codex:${raw}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexRawId(id) {
|
function codexRawId(id: unknown): string | null {
|
||||||
return id ? String(id).replace(/^codex:/, '') : null;
|
return id ? String(id).replace(/^codex:/, '') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexLineUuid(threadId, lineNum) {
|
function codexLineUuid(threadId: unknown, lineNum: number): string {
|
||||||
return `codex:${codexRawId(threadId)}:${String(lineNum).padStart(6, '0')}`;
|
return `codex:${codexRawId(threadId)}:${String(lineNum).padStart(6, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexCallId(callId) {
|
function codexCallId(callId: unknown): string | null {
|
||||||
if (!callId) return null;
|
if (!callId) return null;
|
||||||
return `codex:${String(callId).replace(/^codex:/, '')}`;
|
return `codex:${String(callId).replace(/^codex:/, '')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexParentThreadId(meta) {
|
function codexParentThreadId(meta: JsonRecord): string | null {
|
||||||
const subagent = meta?.source?.subagent;
|
const subagent = meta?.source?.subagent;
|
||||||
return subagent?.thread_spawn?.parent_thread_id
|
return subagent?.thread_spawn?.parent_thread_id
|
||||||
|| meta?.forked_from_id
|
|| meta?.forked_from_id
|
||||||
@@ -216,20 +239,20 @@ function codexParentThreadId(meta) {
|
|||||||
|| null;
|
|| null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexIsGuardianThread(meta, records = []) {
|
function codexIsGuardianThread(meta: JsonRecord, records: CodexLineRecord[] = []): boolean {
|
||||||
const subagent = meta?.source?.subagent;
|
const subagent = meta?.source?.subagent;
|
||||||
if (subagent?.other === 'guardian') return true;
|
if (subagent?.other === 'guardian') return true;
|
||||||
if (meta?.thread_source !== 'subagent') return false;
|
if (meta?.thread_source !== 'subagent') return false;
|
||||||
return records.some(({ obj }) => obj?.payload?.model === 'codex-auto-review' || obj?.model === 'codex-auto-review');
|
return records.some(({ obj }) => obj?.payload?.model === 'codex-auto-review' || obj?.model === 'codex-auto-review');
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCodexGuardianThreadInfo(filePath) {
|
function readCodexGuardianThreadInfo(filePath: string): { threadRawId: string; lineNum: number } | null {
|
||||||
const records = [];
|
const records: CodexLineRecord[] = [];
|
||||||
let metaRecord = null;
|
let metaRecord: CodexLineRecord | null = null;
|
||||||
let lineNum = 0;
|
let lineNum = 0;
|
||||||
readLines(filePath, (line) => {
|
readLines(filePath, (line) => {
|
||||||
lineNum++;
|
lineNum++;
|
||||||
let obj;
|
let obj: JsonRecord;
|
||||||
try {
|
try {
|
||||||
obj = JSON.parse(line);
|
obj = JSON.parse(line);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -243,30 +266,32 @@ function readCodexGuardianThreadInfo(filePath) {
|
|||||||
}
|
}
|
||||||
if (metaRecord && codexIsGuardianThread(metaRecord.obj.payload, records)) return false;
|
if (metaRecord && codexIsGuardianThread(metaRecord.obj.payload, records)) return false;
|
||||||
});
|
});
|
||||||
const meta = metaRecord?.obj?.payload;
|
const capturedMeta = metaRecord as CodexLineRecord | null;
|
||||||
|
const meta = capturedMeta?.obj?.payload;
|
||||||
if (!meta || !codexIsGuardianThread(meta, records)) return null;
|
if (!meta || !codexIsGuardianThread(meta, records)) return null;
|
||||||
return { threadRawId: codexRawId(meta.id), lineNum };
|
const threadRawId = codexRawId(meta.id);
|
||||||
|
return threadRawId ? { threadRawId, lineNum } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexAgentNickname(meta) {
|
function codexAgentNickname(meta: JsonRecord): string | null {
|
||||||
return meta?.agent_nickname
|
return meta?.agent_nickname
|
||||||
|| meta?.source?.subagent?.thread_spawn?.agent_nickname
|
|| meta?.source?.subagent?.thread_spawn?.agent_nickname
|
||||||
|| null;
|
|| null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexAgentRole(meta) {
|
function codexAgentRole(meta: JsonRecord): string | null {
|
||||||
return meta?.agent_role
|
return meta?.agent_role
|
||||||
|| meta?.source?.subagent?.thread_spawn?.agent_role
|
|| meta?.source?.subagent?.thread_spawn?.agent_role
|
||||||
|| null;
|
|| null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCodexJsonInput(value) {
|
function parseCodexJsonInput(value: JsonValue): JsonValue {
|
||||||
if (value === null || value === undefined || value === '') return {};
|
if (value === null || value === undefined || value === '') return {};
|
||||||
if (typeof value !== 'string') return value;
|
if (typeof value !== 'string') return value;
|
||||||
try { return JSON.parse(value); } catch { return value; }
|
try { return JSON.parse(value); } catch { return value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexUsage(payload) {
|
function codexUsage(payload: JsonRecord) {
|
||||||
const usage = payload?.info?.last_token_usage || payload?.info?.total_token_usage || payload?.last_token_usage || null;
|
const usage = payload?.info?.last_token_usage || payload?.info?.total_token_usage || payload?.last_token_usage || null;
|
||||||
if (!usage) return {};
|
if (!usage) return {};
|
||||||
return {
|
return {
|
||||||
@@ -275,37 +300,37 @@ function codexUsage(payload) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexEventText(payload) {
|
function codexEventText(payload: JsonRecord): string | null {
|
||||||
if (typeof payload?.message === 'string') return payload.message;
|
if (typeof payload?.message === 'string') return payload.message;
|
||||||
if (Array.isArray(payload?.text_elements) && payload.text_elements.length) {
|
if (Array.isArray(payload?.text_elements) && payload.text_elements.length) {
|
||||||
const parts = payload.text_elements.map(item => typeof item === 'string' ? item : item?.text).filter(Boolean);
|
const parts = payload.text_elements.map((item: JsonValue) => typeof item === 'string' ? item : item?.text).filter(Boolean);
|
||||||
if (parts.length) return parts.join('\n');
|
if (parts.length) return parts.join('\n');
|
||||||
}
|
}
|
||||||
if (typeof payload?.text === 'string') return payload.text;
|
if (typeof payload?.text === 'string') return payload.text;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexMessagePayloadText(payload) {
|
function codexMessagePayloadText(payload: JsonRecord): string | null {
|
||||||
if (!Array.isArray(payload?.content)) return null;
|
if (!Array.isArray(payload?.content)) return null;
|
||||||
const parts = [];
|
const parts: string[] = [];
|
||||||
for (const block of payload.content) {
|
for (const block of payload.content) {
|
||||||
if (typeof block?.text === 'string') parts.push(block.text);
|
if (typeof block?.text === 'string') parts.push(block.text);
|
||||||
}
|
}
|
||||||
return parts.length ? parts.join('\n') : null;
|
return parts.length ? parts.join('\n') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexVisibleMessageKey(role, text) {
|
function codexVisibleMessageKey(role: unknown, text: unknown): string {
|
||||||
return `${role || ''}\u0000${text || ''}`;
|
return `${role || ''}\u0000${text || ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexToolInput(payload) {
|
function codexToolInput(payload: JsonRecord): JsonValue {
|
||||||
if (payload?.type === 'custom_tool_call') return parseCodexJsonInput(payload.input);
|
if (payload?.type === 'custom_tool_call') return parseCodexJsonInput(payload.input);
|
||||||
if (payload?.type === 'tool_search_call') return parseCodexJsonInput(payload.arguments);
|
if (payload?.type === 'tool_search_call') return parseCodexJsonInput(payload.arguments);
|
||||||
if (payload?.type === 'web_search_call') return { action: payload.action || null };
|
if (payload?.type === 'web_search_call') return { action: payload.action || null };
|
||||||
return parseCodexJsonInput(payload?.arguments);
|
return parseCodexJsonInput(payload?.arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
function codexToolOutput(payload) {
|
function codexToolOutput(payload: JsonRecord): string | null {
|
||||||
if (typeof payload?.output === 'string') return payload.output;
|
if (typeof payload?.output === 'string') return payload.output;
|
||||||
if (payload?.output !== undefined) return JSON.stringify(payload.output);
|
if (payload?.output !== undefined) return JSON.stringify(payload.output);
|
||||||
if (payload?.tools !== undefined) return JSON.stringify(payload.tools);
|
if (payload?.tools !== undefined) return JSON.stringify(payload.tools);
|
||||||
@@ -13,7 +13,7 @@ const fs = require('node:fs');
|
|||||||
import {
|
import {
|
||||||
extractText, extractContentType, extractMessageIsMeta,
|
extractText, extractContentType, extractMessageIsMeta,
|
||||||
filePath, trunc, truncJson, readLines, discoverJsonlFiles,
|
filePath, trunc, truncJson, readLines, discoverJsonlFiles,
|
||||||
} from '../parsing.mjs';
|
} from '../parsing.ts';
|
||||||
|
|
||||||
import type { Cursor, DiscoverContext, IndexRecord, IndexUnit, Provider } from './types.ts';
|
import type { Cursor, DiscoverContext, IndexRecord, IndexUnit, Provider } from './types.ts';
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
codexIsGuardianThread, codexAgentNickname, codexAgentRole, codexUsage,
|
codexIsGuardianThread, codexAgentNickname, codexAgentRole, codexUsage,
|
||||||
codexEventText, codexMessagePayloadText, codexVisibleMessageKey,
|
codexEventText, codexMessagePayloadText, codexVisibleMessageKey,
|
||||||
codexToolInput, codexToolOutput,
|
codexToolInput, codexToolOutput,
|
||||||
} from '../parsing.mjs';
|
} from '../parsing.ts';
|
||||||
|
|
||||||
import type { Cursor, DiscoverContext, IndexRecord, IndexUnit, MessageRecord, Provider } from './types.ts';
|
import type { Cursor, DiscoverContext, IndexRecord, IndexUnit, MessageRecord, Provider } from './types.ts';
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,58 @@
|
|||||||
// Query and attune sandbox helpers for the Core package.
|
// Query and attune sandbox helpers for the Core package.
|
||||||
import { readLines, fs, path } from './db.mjs';
|
import { readLines, fs, path } from './db.ts';
|
||||||
|
|
||||||
function normalizeOpts(optsOrScalar, scalarKey = 'sessionId') {
|
type SqliteDb = any;
|
||||||
|
type DbRow = Record<string, any>;
|
||||||
|
|
||||||
|
interface QueryOptions extends Record<string, any> {
|
||||||
|
limit?: number;
|
||||||
|
sessionId?: string;
|
||||||
|
sessions?: string[];
|
||||||
|
project?: string;
|
||||||
|
after?: string;
|
||||||
|
before?: string;
|
||||||
|
cwd?: string;
|
||||||
|
branch?: string;
|
||||||
|
source?: string;
|
||||||
|
includeMeta?: boolean;
|
||||||
|
query?: string;
|
||||||
|
projectLimit?: number;
|
||||||
|
memoryLimit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ColumnAliases {
|
||||||
|
sessionId: string;
|
||||||
|
project: string;
|
||||||
|
timestamp: string;
|
||||||
|
branch: string;
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RememberInput {
|
||||||
|
path: string;
|
||||||
|
session_id?: string;
|
||||||
|
message_start?: string;
|
||||||
|
message_end?: string;
|
||||||
|
summary: string;
|
||||||
|
project?: string;
|
||||||
|
anchors?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForgetInput {
|
||||||
|
id: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOpts(optsOrScalar: QueryOptions | string | number | null | undefined, scalarKey = 'sessionId'): QueryOptions {
|
||||||
if (optsOrScalar == null) return {};
|
if (optsOrScalar == null) return {};
|
||||||
if (typeof optsOrScalar === 'string') return { [scalarKey]: optsOrScalar };
|
if (typeof optsOrScalar === 'string') return { [scalarKey]: optsOrScalar };
|
||||||
if (typeof optsOrScalar === 'number') return { limit: optsOrScalar };
|
if (typeof optsOrScalar === 'number') return { limit: optsOrScalar };
|
||||||
return optsOrScalar;
|
return optsOrScalar;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWhere(opts, aliases) {
|
function buildWhere(opts: QueryOptions, aliases: ColumnAliases) {
|
||||||
const clauses = [];
|
const clauses: string[] = [];
|
||||||
const params = [];
|
const params: any[] = [];
|
||||||
if (opts.sessionId) { clauses.push(`${aliases.sessionId} = ?`); params.push(opts.sessionId); }
|
if (opts.sessionId) { clauses.push(`${aliases.sessionId} = ?`); params.push(opts.sessionId); }
|
||||||
if (opts.sessions?.length) {
|
if (opts.sessions?.length) {
|
||||||
clauses.push(`${aliases.sessionId} IN (${opts.sessions.map(() => '?').join(',')})`);
|
clauses.push(`${aliases.sessionId} IN (${opts.sessions.map(() => '?').join(',')})`);
|
||||||
@@ -29,7 +71,7 @@ function buildWhere(opts, aliases) {
|
|||||||
|
|
||||||
const BASH_EXIT_PAT = 'Exit code %';
|
const BASH_EXIT_PAT = 'Exit code %';
|
||||||
|
|
||||||
function assertReadOnlySql(sql) {
|
function assertReadOnlySql(sql: unknown): void {
|
||||||
const text = String(sql || '').trim();
|
const text = String(sql || '').trim();
|
||||||
if (!/^(SELECT|WITH)\b/i.test(text)) {
|
if (!/^(SELECT|WITH)\b/i.test(text)) {
|
||||||
throw new Error('sql() only supports read-only SELECT/WITH queries');
|
throw new Error('sql() only supports read-only SELECT/WITH queries');
|
||||||
@@ -41,7 +83,7 @@ function assertReadOnlySql(sql) {
|
|||||||
|
|
||||||
const CJK_TEXT_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
const CJK_TEXT_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
||||||
|
|
||||||
function assertEnglishMemoryText(value, label) {
|
function assertEnglishMemoryText(value: unknown, label: string): void {
|
||||||
const text = String(value || '');
|
const text = String(value || '');
|
||||||
if (!text.trim()) return;
|
if (!text.trim()) return;
|
||||||
if (CJK_TEXT_RE.test(text)) {
|
if (CJK_TEXT_RE.test(text)) {
|
||||||
@@ -50,7 +92,7 @@ function assertEnglishMemoryText(value, label) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSafeFtsQuery(text) {
|
function buildSafeFtsQuery(text: unknown): string {
|
||||||
const tokens = String(text || '').match(/[\p{Letter}\p{Number}]+/gu) || [];
|
const tokens = String(text || '').match(/[\p{Letter}\p{Number}]+/gu) || [];
|
||||||
return tokens
|
return tokens
|
||||||
.slice(0, 12)
|
.slice(0, 12)
|
||||||
@@ -58,23 +100,23 @@ function buildSafeFtsQuery(text) {
|
|||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function createQueryApi(db) {
|
function createQueryApi(db: SqliteDb) {
|
||||||
const q = (sql, ...p) => {
|
const q = (sql: string, ...p: any[]) => {
|
||||||
assertReadOnlySql(sql);
|
assertReadOnlySql(sql);
|
||||||
return db.prepare(sql).all(...p);
|
return db.prepare(sql).all(...p);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeOverviewOpts = (optsOrScalar) => {
|
const normalizeOverviewOpts = (optsOrScalar: QueryOptions | string | number | null | undefined): QueryOptions => {
|
||||||
if (optsOrScalar == null) return {};
|
if (optsOrScalar == null) return {};
|
||||||
if (typeof optsOrScalar === 'string') return { project: optsOrScalar };
|
if (typeof optsOrScalar === 'string') return { project: optsOrScalar };
|
||||||
if (typeof optsOrScalar === 'number') return { limit: optsOrScalar };
|
if (typeof optsOrScalar === 'number') return { limit: optsOrScalar };
|
||||||
return optsOrScalar;
|
return optsOrScalar;
|
||||||
};
|
};
|
||||||
|
|
||||||
const search = (text, opts = {}) => {
|
const search = (text: string, opts: QueryOptions = {}) => {
|
||||||
const { limit = 20, sessionId, project, after, before, cwd, source, includeMeta = false } = opts;
|
const { limit = 20, sessionId, project, after, before, cwd, source, includeMeta = false } = opts;
|
||||||
let where = 'WHERE mf.text MATCH ?';
|
let where = 'WHERE mf.text MATCH ?';
|
||||||
const filterParams = [];
|
const filterParams: any[] = [];
|
||||||
if (sessionId) { where += ' AND mf.session_id=?'; filterParams.push(sessionId); }
|
if (sessionId) { where += ' AND mf.session_id=?'; filterParams.push(sessionId); }
|
||||||
if (project) { where += ' AND s.project LIKE ?'; filterParams.push(project); }
|
if (project) { where += ' AND s.project LIKE ?'; filterParams.push(project); }
|
||||||
if (after) { where += ' AND m.timestamp>?'; filterParams.push(after); }
|
if (after) { where += ' AND m.timestamp>?'; filterParams.push(after); }
|
||||||
@@ -89,7 +131,7 @@ function createQueryApi(db) {
|
|||||||
rank
|
rank
|
||||||
FROM messages_fts mf JOIN messages m ON m.uuid=mf.uuid LEFT JOIN sessions s ON s.id=m.session_id
|
FROM messages_fts mf JOIN messages m ON m.uuid=mf.uuid LEFT JOIN sessions s ON s.id=m.session_id
|
||||||
${where} ORDER BY rank LIMIT ?`);
|
${where} ORDER BY rank LIMIT ?`);
|
||||||
const runMatch = (matchText) => stmt.all(matchText, ...filterParams, limit);
|
const runMatch = (matchText: string): DbRow[] => stmt.all(matchText, ...filterParams, limit);
|
||||||
// Honor raw FTS5 syntax when the query is valid, but never crash on ordinary
|
// Honor raw FTS5 syntax when the query is valid, but never crash on ordinary
|
||||||
// input (hyphens, punctuation) that FTS5 would parse as operators: fall back
|
// input (hyphens, punctuation) that FTS5 would parse as operators: fall back
|
||||||
// to safe per-token quoting, the same tokenization memories() uses.
|
// to safe per-token quoting, the same tokenization memories() uses.
|
||||||
@@ -100,11 +142,11 @@ function createQueryApi(db) {
|
|||||||
const safe = buildSafeFtsQuery(text);
|
const safe = buildSafeFtsQuery(text);
|
||||||
rows = safe ? runMatch(safe) : [];
|
rows = safe ? runMatch(safe) : [];
|
||||||
}
|
}
|
||||||
return rows.map(r => {
|
return rows.map((r: DbRow) => {
|
||||||
const metaClause = includeMeta ? '' : 'AND COALESCE(is_meta,0)=0';
|
const metaClause = includeMeta ? '' : 'AND COALESCE(is_meta,0)=0';
|
||||||
const ctx = db.prepare(
|
const ctx = db.prepare(
|
||||||
`SELECT uuid,text,content_type,is_meta,role,timestamp,model,COALESCE(source, 'claude') as source FROM messages WHERE session_id=? AND uuid!=? ${metaClause} ORDER BY ABS(JULIANDAY(timestamp)-JULIANDAY(?)) LIMIT 6`
|
`SELECT uuid,text,content_type,is_meta,role,timestamp,model,COALESCE(source, 'claude') as source FROM messages WHERE session_id=? AND uuid!=? ${metaClause} ORDER BY ABS(JULIANDAY(timestamp)-JULIANDAY(?)) LIMIT 6`
|
||||||
).all(r.session_id, r.uuid, r.timestamp).sort((a,b) => a.timestamp < b.timestamp ? -1 : 1);
|
).all(r.session_id, r.uuid, r.timestamp).sort((a: DbRow, b: DbRow) => a.timestamp < b.timestamp ? -1 : 1);
|
||||||
const sourceValue = r.m_source || r.s_source || 'claude';
|
const sourceValue = r.m_source || r.s_source || 'claude';
|
||||||
return {
|
return {
|
||||||
message: { uuid: r.uuid, text: r.text, content_type: r.content_type, is_meta: r.is_meta || 0, role: r.role, timestamp: r.timestamp, model: r.model, cwd: r.cwd, source: sourceValue },
|
message: { uuid: r.uuid, text: r.text, content_type: r.content_type, is_meta: r.is_meta || 0, role: r.role, timestamp: r.timestamp, model: r.model, cwd: r.cwd, source: sourceValue },
|
||||||
@@ -115,14 +157,14 @@ function createQueryApi(db) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const context = (uuid) => {
|
const context = (uuid: string) => {
|
||||||
const msg = db.prepare('SELECT * FROM messages WHERE uuid=?').get(uuid);
|
const msg = db.prepare('SELECT * FROM messages WHERE uuid=?').get(uuid);
|
||||||
if (!msg) return null;
|
if (!msg) return null;
|
||||||
const session = db.prepare('SELECT * FROM sessions WHERE id=?').get(msg.session_id);
|
const session = db.prepare('SELECT * FROM sessions WHERE id=?').get(msg.session_id);
|
||||||
const chain = [];
|
const chain: DbRow[] = [];
|
||||||
let cur = msg;
|
let cur = msg;
|
||||||
while (cur?.parent_uuid) { cur = db.prepare('SELECT * FROM messages WHERE uuid=?').get(cur.parent_uuid); if (cur) chain.unshift(cur); }
|
while (cur?.parent_uuid) { cur = db.prepare('SELECT * FROM messages WHERE uuid=?').get(cur.parent_uuid); if (cur) chain.unshift(cur); }
|
||||||
let subagent = msg.agent_id ? db.prepare('SELECT * FROM subagents WHERE agent_id=?').get(msg.agent_id) : null;
|
const subagent = msg.agent_id ? db.prepare('SELECT * FROM subagents WHERE agent_id=?').get(msg.agent_id) : null;
|
||||||
let workflow = null;
|
let workflow = null;
|
||||||
if (msg.agent_id) {
|
if (msg.agent_id) {
|
||||||
const wa = db.prepare('SELECT * FROM workflow_agents WHERE agent_id=?').get(msg.agent_id);
|
const wa = db.prepare('SELECT * FROM workflow_agents WHERE agent_id=?').get(msg.agent_id);
|
||||||
@@ -131,33 +173,33 @@ function createQueryApi(db) {
|
|||||||
return { message: msg, parentChain: chain, session, subagent, workflow };
|
return { message: msg, parentChain: chain, session, subagent, workflow };
|
||||||
};
|
};
|
||||||
|
|
||||||
const trace = (uuid) => {
|
const trace = (uuid: string) => {
|
||||||
const chain = [];
|
const chain: DbRow[] = [];
|
||||||
let cur = db.prepare('SELECT * FROM messages WHERE uuid=?').get(uuid);
|
let cur = db.prepare('SELECT * FROM messages WHERE uuid=?').get(uuid);
|
||||||
while (cur) { chain.unshift(cur); cur = cur.parent_uuid ? db.prepare('SELECT * FROM messages WHERE uuid=?').get(cur.parent_uuid) : null; }
|
while (cur) { chain.unshift(cur); cur = cur.parent_uuid ? db.prepare('SELECT * FROM messages WHERE uuid=?').get(cur.parent_uuid) : null; }
|
||||||
return chain;
|
return chain;
|
||||||
};
|
};
|
||||||
|
|
||||||
const thread = (sid, opts = {}) => {
|
const thread = (sid: string, opts: QueryOptions = {}) => {
|
||||||
const includeMeta = opts?.includeMeta === true;
|
const includeMeta = opts?.includeMeta === true;
|
||||||
const metaClause = includeMeta ? '' : 'AND COALESCE(is_meta,0)=0';
|
const metaClause = includeMeta ? '' : 'AND COALESCE(is_meta,0)=0';
|
||||||
return db.prepare(`SELECT * FROM messages WHERE session_id=? ${metaClause} ORDER BY timestamp`).all(sid);
|
return db.prepare(`SELECT * FROM messages WHERE session_id=? ${metaClause} ORDER BY timestamp`).all(sid);
|
||||||
};
|
};
|
||||||
|
|
||||||
const subagents = (optsOrSid) => {
|
const subagents = (optsOrSid?: QueryOptions | string) => {
|
||||||
const opts = normalizeOpts(optsOrSid);
|
const opts = normalizeOpts(optsOrSid);
|
||||||
const { limit = 100 } = opts;
|
const { limit = 100 } = opts;
|
||||||
const needsJoin = opts.project || opts.branch || opts.source;
|
const needsJoin = opts.project || opts.branch || opts.source;
|
||||||
const { where, params } = buildWhere(opts, { sessionId: 'sa.session_id', project: 's.project', timestamp: 'sa.session_id', branch: 's.git_branch', source: 's.source' });
|
const { where, params } = buildWhere(opts, { sessionId: 'sa.session_id', project: 's.project', timestamp: 'sa.session_id', branch: 's.git_branch', source: 's.source' });
|
||||||
params.push(limit);
|
params.push(limit);
|
||||||
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=sa.session_id' : '';
|
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=sa.session_id' : '';
|
||||||
return db.prepare(`SELECT sa.* FROM subagents sa ${join} WHERE ${where} LIMIT ?`).all(...params).map(r => {
|
return db.prepare(`SELECT sa.* FROM subagents sa ${join} WHERE ${where} LIMIT ?`).all(...params).map((r: DbRow) => {
|
||||||
const c = db.prepare('SELECT COUNT(*) as c FROM messages WHERE agent_id=?').get(r.agent_id);
|
const c = db.prepare('SELECT COUNT(*) as c FROM messages WHERE agent_id=?').get(r.agent_id);
|
||||||
return { ...r, messageCount: c?.c || 0 };
|
return { ...r, messageCount: c?.c || 0 };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const workflows = (optsOrSid) => {
|
const workflows = (optsOrSid?: QueryOptions | string) => {
|
||||||
const opts = normalizeOpts(optsOrSid);
|
const opts = normalizeOpts(optsOrSid);
|
||||||
const { limit = 100 } = opts;
|
const { limit = 100 } = opts;
|
||||||
const needsJoin = opts.project || opts.branch || opts.source;
|
const needsJoin = opts.project || opts.branch || opts.source;
|
||||||
@@ -167,36 +209,36 @@ function createQueryApi(db) {
|
|||||||
return db.prepare(`SELECT w.* FROM workflows w ${join} WHERE ${where} ORDER BY w.timestamp DESC LIMIT ?`).all(...params);
|
return db.prepare(`SELECT w.* FROM workflows w ${join} WHERE ${where} ORDER BY w.timestamp DESC LIMIT ?`).all(...params);
|
||||||
};
|
};
|
||||||
|
|
||||||
const workflowTree = (runId) => {
|
const workflowTree = (runId: string) => {
|
||||||
const wf = db.prepare('SELECT * FROM workflows WHERE run_id=?').get(runId);
|
const wf = db.prepare('SELECT * FROM workflows WHERE run_id=?').get(runId);
|
||||||
if (!wf) return null;
|
if (!wf) return null;
|
||||||
let result = null;
|
let result = null;
|
||||||
try { result = JSON.parse(wf.result_json); } catch {}
|
try { result = JSON.parse(wf.result_json); } catch { /* keep the raw result nullable */ }
|
||||||
const agents = db.prepare('SELECT * FROM workflow_agents WHERE run_id=?').all(runId).map(a => {
|
const agents = db.prepare('SELECT * FROM workflow_agents WHERE run_id=?').all(runId).map((a: DbRow) => {
|
||||||
const mc = db.prepare('SELECT COUNT(*) as c FROM messages WHERE agent_id=?').get(a.agent_id);
|
const mc = db.prepare('SELECT COUNT(*) as c FROM messages WHERE agent_id=?').get(a.agent_id);
|
||||||
return { ...a, messageCount: mc?.c || 0 };
|
return { ...a, messageCount: mc?.c || 0 };
|
||||||
});
|
});
|
||||||
return { ...wf, result, agents };
|
return { ...wf, result, agents };
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileHistory = (fp, opts = {}) => {
|
const fileHistory = (fp: string, opts: QueryOptions = {}) => {
|
||||||
const { limit = 200, after, before, source } = opts;
|
const { limit = 200, after, before, source } = opts;
|
||||||
let where = 'tc.file_path=?';
|
let where = 'tc.file_path=?';
|
||||||
const params = [fp];
|
const params: any[] = [fp];
|
||||||
if (after) { where += ' AND m.timestamp > ?'; params.push(after); }
|
if (after) { where += ' AND m.timestamp > ?'; params.push(after); }
|
||||||
if (before) { where += ' AND m.timestamp < ?'; params.push(before); }
|
if (before) { where += ' AND m.timestamp < ?'; params.push(before); }
|
||||||
if (source && source !== 'all') { where += " AND COALESCE(s.source, 'claude') = ?"; params.push(source); }
|
if (source && source !== 'all') { where += " AND COALESCE(s.source, 'claude') = ?"; params.push(source); }
|
||||||
params.push(limit);
|
params.push(limit);
|
||||||
return db.prepare(
|
return db.prepare(
|
||||||
`SELECT tc.*,s.title as s_title,s.project as s_project,m.timestamp as ts FROM tool_calls tc LEFT JOIN sessions s ON s.id=tc.session_id LEFT JOIN messages m ON m.uuid=tc.message_uuid WHERE ${where} ORDER BY m.timestamp LIMIT ?`
|
`SELECT tc.*,s.title as s_title,s.project as s_project,m.timestamp as ts FROM tool_calls tc LEFT JOIN sessions s ON s.id=tc.session_id LEFT JOIN messages m ON m.uuid=tc.message_uuid WHERE ${where} ORDER BY m.timestamp LIMIT ?`
|
||||||
).all(...params).map(r => ({
|
).all(...params).map((r: DbRow) => ({
|
||||||
toolCall: { id: r.id, message_uuid: r.message_uuid, name: r.name, input_json: r.input_json },
|
toolCall: { id: r.id, message_uuid: r.message_uuid, name: r.name, input_json: r.input_json },
|
||||||
session: { id: r.session_id, title: r.s_title, project: r.s_project },
|
session: { id: r.session_id, title: r.s_title, project: r.s_project },
|
||||||
timestamp: r.ts,
|
timestamp: r.ts,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const failures = (optsOrSid) => {
|
const failures = (optsOrSid?: QueryOptions | string) => {
|
||||||
const opts = normalizeOpts(optsOrSid);
|
const opts = normalizeOpts(optsOrSid);
|
||||||
const { limit = 50 } = opts;
|
const { limit = 50 } = opts;
|
||||||
const needsJoin = opts.project || opts.branch || opts.source;
|
const needsJoin = opts.project || opts.branch || opts.source;
|
||||||
@@ -205,7 +247,7 @@ function createQueryApi(db) {
|
|||||||
const errorCond = `(tr.is_error = 1 OR tr.content LIKE '${BASH_EXIT_PAT}')`;
|
const errorCond = `(tr.is_error = 1 OR tr.content LIKE '${BASH_EXIT_PAT}')`;
|
||||||
const allParams = [...filterParams, limit];
|
const allParams = [...filterParams, limit];
|
||||||
const rows = db.prepare(`SELECT tr.* FROM tool_results tr ${join} LEFT JOIN messages rm ON rm.uuid=tr.message_uuid WHERE ${errorCond} AND ${where} ORDER BY rm.timestamp DESC LIMIT ?`).all(...allParams);
|
const rows = db.prepare(`SELECT tr.* FROM tool_results tr ${join} LEFT JOIN messages rm ON rm.uuid=tr.message_uuid WHERE ${errorCond} AND ${where} ORDER BY rm.timestamp DESC LIMIT ?`).all(...allParams);
|
||||||
return rows.map(r => {
|
return rows.map((r: DbRow) => {
|
||||||
const tc = db.prepare('SELECT * FROM tool_calls WHERE id=?').get(r.tool_use_id);
|
const tc = db.prepare('SELECT * FROM tool_calls WHERE id=?').get(r.tool_use_id);
|
||||||
const session = db.prepare('SELECT * FROM sessions WHERE id=?').get(r.session_id);
|
const session = db.prepare('SELECT * FROM sessions WHERE id=?').get(r.session_id);
|
||||||
const rm = db.prepare('SELECT * FROM messages WHERE uuid=?').get(r.message_uuid);
|
const rm = db.prepare('SELECT * FROM messages WHERE uuid=?').get(r.message_uuid);
|
||||||
@@ -214,7 +256,7 @@ function createQueryApi(db) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const sessions = (optsOrN) => {
|
const sessions = (optsOrN?: QueryOptions | number | string) => {
|
||||||
const opts = normalizeOpts(optsOrN, 'sessionId');
|
const opts = normalizeOpts(optsOrN, 'sessionId');
|
||||||
const { limit = 50 } = opts;
|
const { limit = 50 } = opts;
|
||||||
const { where, params } = buildWhere(opts, { sessionId: 's.id', project: 's.project', timestamp: 's.started_at', branch: 's.git_branch', source: 's.source' });
|
const { where, params } = buildWhere(opts, { sessionId: 's.id', project: 's.project', timestamp: 's.started_at', branch: 's.git_branch', source: 's.source' });
|
||||||
@@ -224,7 +266,7 @@ function createQueryApi(db) {
|
|||||||
|
|
||||||
const recent = (n = 10) => sessions({ limit: n });
|
const recent = (n = 10) => sessions({ limit: n });
|
||||||
|
|
||||||
const summaries = (optsOrSid) => {
|
const summaries = (optsOrSid?: QueryOptions | string) => {
|
||||||
const opts = normalizeOpts(optsOrSid);
|
const opts = normalizeOpts(optsOrSid);
|
||||||
const { limit = 100 } = opts;
|
const { limit = 100 } = opts;
|
||||||
const { where, params } = buildWhere(opts, { sessionId: 'su.session_id', project: 's.project', timestamp: 'su.timestamp', branch: 's.git_branch', source: 's.source' });
|
const { where, params } = buildWhere(opts, { sessionId: 'su.session_id', project: 's.project', timestamp: 'su.timestamp', branch: 's.git_branch', source: 's.source' });
|
||||||
@@ -232,21 +274,21 @@ function createQueryApi(db) {
|
|||||||
return db.prepare(`SELECT su.*, s.title as session_title, s.project FROM summaries su LEFT JOIN sessions s ON s.id=su.session_id WHERE ${where} ORDER BY su.timestamp DESC LIMIT ?`).all(...params);
|
return db.prepare(`SELECT su.*, s.title as session_title, s.project FROM summaries su LEFT JOIN sessions s ON s.id=su.session_id WHERE ${where} ORDER BY su.timestamp DESC LIMIT ?`).all(...params);
|
||||||
};
|
};
|
||||||
|
|
||||||
const overview = (optsOrScalar) => {
|
const overview = (optsOrScalar?: QueryOptions | string | number) => {
|
||||||
const opts = normalizeOverviewOpts(optsOrScalar);
|
const opts = normalizeOverviewOpts(optsOrScalar);
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const sessionLimit = opts.limit ?? 8;
|
const sessionLimit = opts.limit ?? 8;
|
||||||
const projectLimit = opts.projectLimit ?? 20;
|
const projectLimit = opts.projectLimit ?? 20;
|
||||||
const memoryLimit = opts.memoryLimit ?? 100;
|
const memoryLimit = opts.memoryLimit ?? 100;
|
||||||
|
|
||||||
const projectDescriptor = (row, source, confidence) => row ? ({
|
const projectDescriptor = (row: DbRow | null, source: string, confidence: string) => row ? ({
|
||||||
project: row.project,
|
project: row.project,
|
||||||
project_path: row.project_path || null,
|
project_path: row.project_path || null,
|
||||||
source,
|
source,
|
||||||
confidence,
|
confidence,
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
const latestProjectByPattern = (pattern) => {
|
const latestProjectByPattern = (pattern: string): DbRow | undefined => {
|
||||||
const fromSessions = db.prepare(`
|
const fromSessions = db.prepare(`
|
||||||
SELECT project, project_path
|
SELECT project, project_path
|
||||||
FROM sessions
|
FROM sessions
|
||||||
@@ -278,8 +320,8 @@ function createQueryApi(db) {
|
|||||||
GROUP BY project, project_path
|
GROUP BY project, project_path
|
||||||
`).all();
|
`).all();
|
||||||
const byProjectPath = paths
|
const byProjectPath = paths
|
||||||
.filter(r => cwd === r.project_path || cwd.startsWith(r.project_path + path.sep))
|
.filter((r: DbRow) => cwd === r.project_path || cwd.startsWith(r.project_path + path.sep))
|
||||||
.sort((a, b) => b.project_path.length - a.project_path.length || String(b.last_seen || '').localeCompare(String(a.last_seen || '')))[0];
|
.sort((a: DbRow, b: DbRow) => b.project_path.length - a.project_path.length || String(b.last_seen || '').localeCompare(String(a.last_seen || '')))[0];
|
||||||
if (byProjectPath) return projectDescriptor(byProjectPath, 'cwd_project_path', 'exact');
|
if (byProjectPath) return projectDescriptor(byProjectPath, 'cwd_project_path', 'exact');
|
||||||
|
|
||||||
const byMessageCwd = db.prepare(`
|
const byMessageCwd = db.prepare(`
|
||||||
@@ -332,7 +374,7 @@ function createQueryApi(db) {
|
|||||||
LEFT JOIN memory_stats ms ON ms.project = n.project
|
LEFT JOIN memory_stats ms ON ms.project = n.project
|
||||||
ORDER BY COALESCE(ss.last_session_at, ms.last_memory_at) DESC
|
ORDER BY COALESCE(ss.last_session_at, ms.last_memory_at) DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(projectLimit).map(row => {
|
`).all(projectLimit).map((row: DbRow) => {
|
||||||
const branches = db.prepare(`
|
const branches = db.prepare(`
|
||||||
SELECT git_branch
|
SELECT git_branch
|
||||||
FROM sessions
|
FROM sessions
|
||||||
@@ -340,7 +382,7 @@ function createQueryApi(db) {
|
|||||||
GROUP BY git_branch
|
GROUP BY git_branch
|
||||||
ORDER BY MAX(COALESCE(ended_at, started_at)) DESC
|
ORDER BY MAX(COALESCE(ended_at, started_at)) DESC
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
`).all(row.project).map(r => r.git_branch);
|
`).all(row.project).map((r: DbRow) => r.git_branch);
|
||||||
return { ...row, recent_branches: branches };
|
return { ...row, recent_branches: branches };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -408,7 +450,7 @@ function createQueryApi(db) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveJsonlPath = (messageUuid) => {
|
const resolveJsonlPath = (messageUuid: string): string | null => {
|
||||||
const msg = db.prepare('SELECT session_id, agent_id, source FROM messages WHERE uuid=?').get(messageUuid);
|
const msg = db.prepare('SELECT session_id, agent_id, source FROM messages WHERE uuid=?').get(messageUuid);
|
||||||
if (!msg) return null;
|
if (!msg) return null;
|
||||||
if (msg.source === 'codex' || String(messageUuid).startsWith('codex:')) {
|
if (msg.source === 'codex' || String(messageUuid).startsWith('codex:')) {
|
||||||
@@ -444,7 +486,7 @@ function createQueryApi(db) {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findCodexRawLine = (jsonlPath, uuid) => {
|
const findCodexRawLine = (jsonlPath: string | null, uuid: string): string | null => {
|
||||||
const match = /^codex:[^:]+:(\d+)$/.exec(String(uuid));
|
const match = /^codex:[^:]+:(\d+)$/.exec(String(uuid));
|
||||||
if (!match || !jsonlPath || !fs.existsSync(jsonlPath)) return null;
|
if (!match || !jsonlPath || !fs.existsSync(jsonlPath)) return null;
|
||||||
const targetLine = Number(match[1]);
|
const targetLine = Number(match[1]);
|
||||||
@@ -459,18 +501,18 @@ function createQueryApi(db) {
|
|||||||
return found;
|
return found;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findRawLine = (jsonlPath, uuid) => {
|
const findRawLine = (jsonlPath: string | null, uuid: string): string | null => {
|
||||||
if (!jsonlPath || !fs.existsSync(jsonlPath)) return null;
|
if (!jsonlPath || !fs.existsSync(jsonlPath)) return null;
|
||||||
if (String(uuid).startsWith('codex:')) return findCodexRawLine(jsonlPath, uuid);
|
if (String(uuid).startsWith('codex:')) return findCodexRawLine(jsonlPath, uuid);
|
||||||
let found = null;
|
let found = null;
|
||||||
readLines(jsonlPath, (line) => {
|
readLines(jsonlPath, (line) => {
|
||||||
if (!line.includes(uuid)) return;
|
if (!line.includes(uuid)) return;
|
||||||
try { const obj = JSON.parse(line); if (obj.uuid === uuid) { found = line; return false; } } catch {}
|
try { const obj = JSON.parse(line); if (obj.uuid === uuid) { found = line; return false; } } catch { /* skip malformed JSONL lines */ }
|
||||||
});
|
});
|
||||||
return found;
|
return found;
|
||||||
};
|
};
|
||||||
|
|
||||||
const raw = (messageUuid, opts = {}) => {
|
const raw = (messageUuid: string, opts: { offset?: number; limit?: number } = {}) => {
|
||||||
const { offset = 0, limit = 10000 } = opts;
|
const { offset = 0, limit = 10000 } = opts;
|
||||||
const jsonlPath = resolveJsonlPath(messageUuid);
|
const jsonlPath = resolveJsonlPath(messageUuid);
|
||||||
const line = findRawLine(jsonlPath, messageUuid);
|
const line = findRawLine(jsonlPath, messageUuid);
|
||||||
@@ -484,7 +526,7 @@ function createQueryApi(db) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const memories = (optsOrSid) => {
|
const memories = (optsOrSid?: QueryOptions | string) => {
|
||||||
const opts = normalizeOpts(optsOrSid);
|
const opts = normalizeOpts(optsOrSid);
|
||||||
const { limit = 50, query } = opts;
|
const { limit = 50, query } = opts;
|
||||||
assertEnglishMemoryText(query, 'memories() query');
|
assertEnglishMemoryText(query, 'memories() query');
|
||||||
@@ -496,7 +538,7 @@ function createQueryApi(db) {
|
|||||||
branch: 's.git_branch',
|
branch: 's.git_branch',
|
||||||
source: 's.source',
|
source: 's.source',
|
||||||
});
|
});
|
||||||
let where = baseWhere + ' AND mem.deleted_at IS NULL';
|
const where = baseWhere + ' AND mem.deleted_at IS NULL';
|
||||||
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=mem.session_id' : '';
|
const join = needsJoin ? 'LEFT JOIN sessions s ON s.id=mem.session_id' : '';
|
||||||
const hasQuery = String(query || '').trim().length > 0;
|
const hasQuery = String(query || '').trim().length > 0;
|
||||||
const ftsQuery = buildSafeFtsQuery(query);
|
const ftsQuery = buildSafeFtsQuery(query);
|
||||||
@@ -521,8 +563,8 @@ function createQueryApi(db) {
|
|||||||
return { sql: q, search, context, trace, thread, subagents, workflows, workflowTree, fileHistory, failures, sessions, recent, summaries, raw, memories, overview };
|
return { sql: q, search, context, trace, thread, subagents, workflows, workflowTree, fileHistory, failures, sessions, recent, summaries, raw, memories, overview };
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAttuneApi(db) {
|
function createAttuneApi(db: SqliteDb) {
|
||||||
const resolveMemoryPath = (memoryPath, sessionId) => {
|
const resolveMemoryPath = (memoryPath: string, sessionId?: string): string => {
|
||||||
let base = null;
|
let base = null;
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
base = db.prepare('SELECT project_path FROM sessions WHERE id=?').get(sessionId)?.project_path || null;
|
base = db.prepare('SELECT project_path FROM sessions WHERE id=?').get(sessionId)?.project_path || null;
|
||||||
@@ -540,7 +582,7 @@ function createAttuneApi(db) {
|
|||||||
return resolved;
|
return resolved;
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAnchors = (anchors) => {
|
const normalizeAnchors = (anchors: unknown): string | null => {
|
||||||
if (anchors == null) return null;
|
if (anchors == null) return null;
|
||||||
let parsed = anchors;
|
let parsed = anchors;
|
||||||
if (typeof anchors === 'string') {
|
if (typeof anchors === 'string') {
|
||||||
@@ -561,7 +603,7 @@ function createAttuneApi(db) {
|
|||||||
return parsed.length ? JSON.stringify(parsed) : null;
|
return parsed.length ? JSON.stringify(parsed) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const remember = ({ path: memoryPath, session_id, message_start, message_end, summary, project, anchors }) => {
|
const remember = ({ path: memoryPath, session_id, message_start, message_end, summary, project, anchors }: RememberInput) => {
|
||||||
if (!memoryPath || !summary) throw new Error('remember() requires path and summary');
|
if (!memoryPath || !summary) throw new Error('remember() requires path and summary');
|
||||||
assertEnglishMemoryText(summary, 'remember() summary');
|
assertEnglishMemoryText(summary, 'remember() summary');
|
||||||
const normalizedPath = resolveMemoryPath(memoryPath, session_id);
|
const normalizedPath = resolveMemoryPath(memoryPath, session_id);
|
||||||
@@ -574,7 +616,7 @@ function createAttuneApi(db) {
|
|||||||
return { id, path: normalizedPath, project: proj, anchors: normalizedAnchors, created_at };
|
return { id, path: normalizedPath, project: proj, anchors: normalizedAnchors, created_at };
|
||||||
};
|
};
|
||||||
|
|
||||||
const forget = ({ id, reason }) => {
|
const forget = ({ id, reason }: ForgetInput) => {
|
||||||
const deletionReason = String(reason || '').trim();
|
const deletionReason = String(reason || '').trim();
|
||||||
if (!id || !deletionReason) throw new Error('forget() requires id and reason');
|
if (!id || !deletionReason) throw new Error('forget() requires id and reason');
|
||||||
const row = db.prepare('SELECT id, deleted_at, deleted_reason FROM memories WHERE id=?').get(id);
|
const row = db.prepare('SELECT id, deleted_at, deleted_reason FROM memories WHERE id=?').get(id);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// Skill transport: a thin CLI shell over the Obelisk Core package.
|
// Skill transport: a typed thin CLI shell over the Obelisk Core package.
|
||||||
// It only parses args, reads script files, prints JSON, and owns the uniform
|
// It only parses args, reads script files, prints JSON, and owns the uniform
|
||||||
// { error, stack } + exit-1 error envelope. All logic lives in Core.
|
// { error, stack } + exit-1 error envelope. All logic lives in Core.
|
||||||
|
|
||||||
@@ -14,11 +14,14 @@ async function main() {
|
|||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
// Uniform error envelope across all four verbs: a failure is reported as
|
// Uniform error envelope across all four verbs: a failure is reported as
|
||||||
// { error, stack } on stdout with exit code 1, never a raw crash on stderr.
|
// { error, stack } on stdout with exit code 1, never a raw crash on stderr.
|
||||||
const fail = (e) => {
|
const fail = (e: unknown): void => {
|
||||||
process.stdout.write(JSON.stringify({ error: e.message, stack: e.stack }) + '\n');
|
const error = e instanceof Error ? e : new Error(String(e));
|
||||||
|
process.stdout.write(JSON.stringify({ error: error.message, stack: error.stack }) + '\n');
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
};
|
};
|
||||||
const emit = (r) => process.stdout.write(JSON.stringify(r, null, 2) + '\n');
|
const emit = (r: unknown): void => {
|
||||||
|
process.stdout.write(JSON.stringify(r, null, 2) + '\n');
|
||||||
|
};
|
||||||
|
|
||||||
if (args[0] === '--build') {
|
if (args[0] === '--build') {
|
||||||
try {
|
try {
|
||||||
@@ -39,7 +42,7 @@ async function main() {
|
|||||||
try { emit(await executeAttune(fs.readFileSync(path.resolve(args[1]), 'utf8'))); } catch (e) { fail(e); }
|
try { emit(await executeAttune(fs.readFileSync(path.resolve(args[1]), 'utf8'))); } catch (e) { fail(e); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
process.stderr.write('Usage:\n node runtime.mjs --build\n node runtime.mjs --search "text"\n node runtime.mjs --query <file.js>\n node runtime.mjs --attune <file.js>\n');
|
process.stderr.write('Usage:\n node runtime.js --build\n node runtime.js --search "text"\n node runtime.js --query <file.js>\n node runtime.js --attune <file.js>\n');
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,5 +7,5 @@
|
|||||||
"declaration": true,
|
"declaration": true,
|
||||||
"rewriteRelativeImportExtensions": true
|
"rewriteRelativeImportExtensions": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.mjs"]
|
"include": ["src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Obelisk -- Helper API Reference
|
# Obelisk -- Helper API Reference
|
||||||
|
|
||||||
Detailed reference for globals available inside `runtime.mjs --query` and
|
Detailed reference for globals available inside `runtime.js --query` and
|
||||||
`runtime.mjs --attune` scripts.
|
`runtime.js --attune` scripts.
|
||||||
|
|
||||||
- Use `references/schema.md` for raw SQL table/field/join checks.
|
- Use `references/schema.md` for raw SQL table/field/join checks.
|
||||||
- Use `references/query-patterns.md` for copyable retrieval plans.
|
- Use `references/query-patterns.md` for copyable retrieval plans.
|
||||||
@@ -16,7 +16,7 @@ memory mutation helpers.
|
|||||||
|
|
||||||
### Read Helpers
|
### Read Helpers
|
||||||
|
|
||||||
These globals are available only in `runtime.mjs --query` scripts:
|
These globals are available only in `runtime.js --query` scripts:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
sql, search, context, trace, thread, raw,
|
sql, search, context, trace, thread, raw,
|
||||||
@@ -31,7 +31,7 @@ helpers is treated as `sessionId`; passing a number is treated as `limit`.
|
|||||||
|
|
||||||
### Mutation Helpers
|
### Mutation Helpers
|
||||||
|
|
||||||
These globals are available only in `runtime.mjs --attune` scripts:
|
These globals are available only in `runtime.js --attune` scripts:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
remember, forget
|
remember, forget
|
||||||
@@ -413,7 +413,7 @@ not a counting primitive.
|
|||||||
#### `remember(record)`
|
#### `remember(record)`
|
||||||
|
|
||||||
Register a human-approved markdown memory file. Available only in
|
Register a human-approved markdown memory file. Available only in
|
||||||
`runtime.mjs --attune` scripts.
|
`runtime.js --attune` scripts.
|
||||||
|
|
||||||
| Param | Type | Description |
|
| Param | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -439,7 +439,7 @@ Returns:
|
|||||||
#### `forget(record)`
|
#### `forget(record)`
|
||||||
|
|
||||||
Archive a human-approved memory record. Available only in
|
Archive a human-approved memory record. Available only in
|
||||||
`runtime.mjs --attune` scripts.
|
`runtime.js --attune` scripts.
|
||||||
|
|
||||||
| Param | Type | Description |
|
| Param | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Obelisk Query Patterns
|
# Obelisk Query Patterns
|
||||||
|
|
||||||
These are copyable CodeAct patterns for `runtime.mjs --query` scripts plus
|
These are copyable CodeAct patterns for `runtime.js --query` scripts plus
|
||||||
`--attune` memory mutation patterns. They are not new APIs. Adapt them to the
|
`--attune` memory mutation patterns. They are not new APIs. Adapt them to the
|
||||||
user's scope and return compact evidence.
|
user's scope and return compact evidence.
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ Use this only after the user approves writing memory and the markdown file
|
|||||||
already exists. `remember()` validates the file and stores a normalized absolute
|
already exists. `remember()` validates the file and stores a normalized absolute
|
||||||
path, so keep the script small and return the registered record.
|
path, so keep the script small and return the registered record.
|
||||||
|
|
||||||
Run this script with `runtime.mjs --attune <script>`. The `--attune` runtime
|
Run this script with `runtime.js --attune <script>`. The `--attune` runtime
|
||||||
exposes only `remember()` and `forget()`, not retrieval helpers.
|
exposes only `remember()` and `forget()`, not retrieval helpers.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -204,7 +204,7 @@ the exact memory ID in a normal `--query` script first. If one candidate clearly
|
|||||||
matches the user's request, that request is approval to archive it; if several
|
matches the user's request, that request is approval to archive it; if several
|
||||||
candidates match, ask which one to forget.
|
candidates match, ask which one to forget.
|
||||||
|
|
||||||
Run the mutation with `runtime.mjs --attune <script>`:
|
Run the mutation with `runtime.js --attune <script>`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
return forget({
|
return forget({
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ function walk(dir) {
|
|||||||
test('build:skill produces a runnable, readable, .ts-free skill artifact', () => {
|
test('build:skill produces a runnable, readable, .ts-free skill artifact', () => {
|
||||||
execFileSync('npm', ['run', 'build:skill'], { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' });
|
execFileSync('npm', ['run', 'build:skill'], { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' });
|
||||||
|
|
||||||
// Structure: compiled Core + copied .mjs + schema + docs + package.json.
|
// Structure: compiled Core + schema + docs + package.json.
|
||||||
for (const rel of [
|
for (const rel of [
|
||||||
'package.json', 'SKILL.md', 'references/api-reference.md',
|
'package.json', 'SKILL.md', 'references/api-reference.md',
|
||||||
'scripts/core.js', 'scripts/persist.js', 'scripts/providers/claude.js',
|
'scripts/core.js', 'scripts/persist.js', 'scripts/providers/claude.js',
|
||||||
'scripts/providers/codex.js', 'scripts/runtime.mjs', 'scripts/indexer.mjs',
|
'scripts/providers/codex.js', 'scripts/runtime.js', 'scripts/indexer.js',
|
||||||
'scripts/db.mjs', 'scripts/parsing.mjs', 'scripts/query.mjs', 'scripts/schema.sql',
|
'scripts/db.js', 'scripts/parsing.js', 'scripts/query.js', 'scripts/schema.sql',
|
||||||
]) {
|
]) {
|
||||||
assert.ok(existsSync(join(skillDir, rel)), `artifact missing ${rel}`);
|
assert.ok(existsSync(join(skillDir, rel)), `artifact missing ${rel}`);
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ test('build:skill produces a runnable, readable, .ts-free skill artifact', () =>
|
|||||||
writeFileSync(join(projDir, 'smoke.jsonl'),
|
writeFileSync(join(projDir, 'smoke.jsonl'),
|
||||||
JSON.stringify({ uuid: 'm1', type: 'user', timestamp: '2026-06-10T10:00:00Z', cwd: '/tmp/proj', message: { role: 'user', content: 'hello artifact' } }) + '\n');
|
JSON.stringify({ uuid: 'm1', type: 'user', timestamp: '2026-06-10T10:00:00Z', cwd: '/tmp/proj', message: { role: 'user', content: 'hello artifact' } }) + '\n');
|
||||||
const env = { ...process.env, HOME: home };
|
const env = { ...process.env, HOME: home };
|
||||||
const runtime = join(skillDir, 'scripts', 'runtime.mjs');
|
const runtime = join(skillDir, 'scripts', 'runtime.js');
|
||||||
|
|
||||||
const build = spawnSync(process.execPath, [runtime, '--build'], { env, encoding: 'utf8' });
|
const build = spawnSync(process.execPath, [runtime, '--build'], { env, encoding: 'utf8' });
|
||||||
assert.equal(build.status, 0, build.stderr || build.stdout);
|
assert.equal(build.status, 0, build.stderr || build.stdout);
|
||||||
@@ -65,6 +65,24 @@ test('build:skill produces a runnable, readable, .ts-free skill artifact', () =>
|
|||||||
assert.equal(search.status, 0, search.stderr || search.stdout);
|
assert.equal(search.status, 0, search.stderr || search.stdout);
|
||||||
const hits = JSON.parse(search.stdout);
|
const hits = JSON.parse(search.stdout);
|
||||||
assert.equal(hits[0]?.message?.text, 'hello artifact', 'compiled artifact indexed and found the message');
|
assert.equal(hits[0]?.message?.text, 'hello artifact', 'compiled artifact indexed and found the message');
|
||||||
|
|
||||||
|
const memoryPath = join(home, 'artifact-memory.md');
|
||||||
|
const attunePath = join(home, 'attune.mjs');
|
||||||
|
writeFileSync(memoryPath, '# Artifact memory\n');
|
||||||
|
writeFileSync(attunePath, `return remember(${JSON.stringify({
|
||||||
|
path: memoryPath,
|
||||||
|
session_id: 'smoke',
|
||||||
|
summary: 'Artifact release smoke memory',
|
||||||
|
})});`);
|
||||||
|
const attune = spawnSync(process.execPath, [runtime, '--attune', attunePath], { env, encoding: 'utf8' });
|
||||||
|
assert.equal(attune.status, 0, attune.stderr || attune.stdout);
|
||||||
|
assert.match(JSON.parse(attune.stdout).id, /^mem-/);
|
||||||
|
|
||||||
|
const queryPath = join(home, 'query.mjs');
|
||||||
|
writeFileSync(queryPath, "return memories({ sessionId: 'smoke', query: 'Artifact release smoke' });");
|
||||||
|
const query = spawnSync(process.execPath, [runtime, '--query', queryPath], { env, encoding: 'utf8' });
|
||||||
|
assert.equal(query.status, 0, query.stderr || query.stdout);
|
||||||
|
assert.equal(JSON.parse(query.stdout)[0]?.summary, 'Artifact release smoke memory');
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(home, { recursive: true, force: true });
|
rmSync(home, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const require = createRequire(import.meta.url);
|
|||||||
const { DatabaseSync } = require('node:sqlite');
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
|
||||||
function runRuntime(args, home) {
|
function runRuntime(args, home) {
|
||||||
return spawnSync(process.execPath, ['packages/core/src/runtime.mjs', ...args], {
|
return spawnSync(process.execPath, ['packages/core/src/runtime.ts', ...args], {
|
||||||
cwd: repoRoot, env: { ...process.env, HOME: home }, encoding: 'utf8',
|
cwd: repoRoot, env: { ...process.env, HOME: home }, encoding: 'utf8',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs';
|
|||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { createQueryApi, createAttuneApi } from '../packages/core/src/query.mjs';
|
import { createQueryApi, createAttuneApi } from '../packages/core/src/query.ts';
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const { DatabaseSync } = require('node:sqlite');
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ test('a passive query does not mutate the index while a fresh daemon owns writes
|
|||||||
|
|
||||||
const queryPath = join(home, 'query.mjs');
|
const queryPath = join(home, 'query.mjs');
|
||||||
writeFileSync(queryPath, "return 'read-only';");
|
writeFileSync(queryPath, "return 'read-only';");
|
||||||
const result = spawnSync(process.execPath, ['packages/core/src/runtime.mjs', '--query', queryPath], {
|
const result = spawnSync(process.execPath, ['packages/core/src/runtime.ts', '--query', queryPath], {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
env: { ...process.env, HOME: home },
|
env: { ...process.env, HOME: home },
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
@@ -55,7 +55,7 @@ test('attune refuses to mutate the index while a fresh daemon owns writes', () =
|
|||||||
|
|
||||||
const attunePath = join(home, 'attune.mjs');
|
const attunePath = join(home, 'attune.mjs');
|
||||||
writeFileSync(attunePath, 'return true;');
|
writeFileSync(attunePath, 'return true;');
|
||||||
const result = spawnSync(process.execPath, ['packages/core/src/runtime.mjs', '--attune', attunePath], {
|
const result = spawnSync(process.execPath, ['packages/core/src/runtime.ts', '--attune', attunePath], {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
env: { ...process.env, HOME: home },
|
env: { ...process.env, HOME: home },
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
@@ -86,7 +86,7 @@ test('a passive query stays read-only when another process holds the writer leas
|
|||||||
try {
|
try {
|
||||||
const queryPath = join(home, 'query.mjs');
|
const queryPath = join(home, 'query.mjs');
|
||||||
writeFileSync(queryPath, "return 'writer-busy';");
|
writeFileSync(queryPath, "return 'writer-busy';");
|
||||||
const result = spawnSync(process.execPath, ['packages/core/src/runtime.mjs', '--query', queryPath], {
|
const result = spawnSync(process.execPath, ['packages/core/src/runtime.ts', '--query', queryPath], {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
env: { ...process.env, HOME: home },
|
env: { ...process.env, HOME: home },
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
@@ -120,7 +120,7 @@ test('a passive query fails closed when daemon ownership cannot be read', () =>
|
|||||||
try {
|
try {
|
||||||
const queryPath = join(home, 'query.mjs');
|
const queryPath = join(home, 'query.mjs');
|
||||||
writeFileSync(queryPath, "return 'ownership-unknown';");
|
writeFileSync(queryPath, "return 'ownership-unknown';");
|
||||||
const result = spawnSync(process.execPath, ['packages/core/src/runtime.mjs', '--query', queryPath], {
|
const result = spawnSync(process.execPath, ['packages/core/src/runtime.ts', '--query', queryPath], {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
env: { ...process.env, HOME: home },
|
env: { ...process.env, HOME: home },
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { test } from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
import { extractContentType, extractMessageIsMeta } from '../packages/core/src/db.mjs';
|
import { extractContentType, extractMessageIsMeta } from '../packages/core/src/db.ts';
|
||||||
|
|
||||||
async function readExecutableSchema() {
|
async function readExecutableSchema() {
|
||||||
return readFile(new URL('../packages/core/src/schema.sql', import.meta.url), 'utf8');
|
return readFile(new URL('../packages/core/src/schema.sql', import.meta.url), 'utf8');
|
||||||
@@ -21,7 +21,7 @@ async function readSkill() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('db module loads the executable schema from scripts/schema.sql', async () => {
|
test('db module loads the executable schema from scripts/schema.sql', async () => {
|
||||||
const source = await readFile(new URL('../packages/core/src/db.mjs', import.meta.url), 'utf8');
|
const source = await readFile(new URL('../packages/core/src/db.ts', import.meta.url), 'utf8');
|
||||||
|
|
||||||
assert.match(source, /schema\.sql/);
|
assert.match(source, /schema\.sql/);
|
||||||
assert.doesNotMatch(source, /CREATE TABLE IF NOT EXISTS sessions/);
|
assert.doesNotMatch(source, /CREATE TABLE IF NOT EXISTS sessions/);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const require = createRequire(import.meta.url);
|
|||||||
const { DatabaseSync } = require('node:sqlite');
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
|
||||||
function runRuntime(args, home) {
|
function runRuntime(args, home) {
|
||||||
return spawnSync(process.execPath, ['packages/core/src/runtime.mjs', ...args], {
|
return spawnSync(process.execPath, ['packages/core/src/runtime.ts', ...args], {
|
||||||
cwd: repoRoot, env: { ...process.env, HOME: home }, encoding: 'utf8',
|
cwd: repoRoot, env: { ...process.env, HOME: home }, encoding: 'utf8',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { test } from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
|
|
||||||
import { inferProjectPath, refreshSessionProjectPaths, shouldSkipBuild } from '../packages/core/src/indexer.mjs';
|
import { inferProjectPath, refreshSessionProjectPaths, shouldSkipBuild } from '../packages/core/src/indexer.ts';
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const { DatabaseSync } = require('node:sqlite');
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { createQueryApi, createAttuneApi } from '../packages/core/src/query.mjs';
|
import { createQueryApi, createAttuneApi } from '../packages/core/src/query.ts';
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const { DatabaseSync } = require('node:sqlite');
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { spawnSync } from 'node:child_process';
|
|||||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
||||||
function runRuntime(args, { home }) {
|
function runRuntime(args, { home }) {
|
||||||
return spawnSync(process.execPath, ['packages/core/src/runtime.mjs', ...args], {
|
return spawnSync(process.execPath, ['packages/core/src/runtime.ts', ...args], {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
env: { ...process.env, HOME: home },
|
env: { ...process.env, HOME: home },
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const require = createRequire(import.meta.url);
|
|||||||
const { DatabaseSync } = require('node:sqlite');
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
|
||||||
function runRuntime(args, { home }) {
|
function runRuntime(args, { home }) {
|
||||||
return spawnSync(process.execPath, ['packages/core/src/runtime.mjs', ...args], {
|
return spawnSync(process.execPath, ['packages/core/src/runtime.ts', ...args], {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
env: { ...process.env, HOME: home },
|
env: { ...process.env, HOME: home },
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
|
|||||||
+1
-1
@@ -7,5 +7,5 @@
|
|||||||
"declaration": false,
|
"declaration": false,
|
||||||
"rewriteRelativeImportExtensions": true
|
"rewriteRelativeImportExtensions": true
|
||||||
},
|
},
|
||||||
"include": ["packages/core/src/**/*.ts", "packages/core/src/**/*.mjs"]
|
"include": ["packages/core/src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user