soulbridge 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **给任何 agent 装上一个人格层。**
4
4
  > A personality layer for any agent — continuity, forgetting, and growth across tools.
5
5
 
6
- **状态:v0.0.4 · 已发布 npm · 接口仍在演进**
6
+ **状态:v0.0.6 · 已发布 npm · 接口仍在演进**
7
7
 
8
8
  ---
9
9
 
@@ -111,6 +111,7 @@ SQLite(默认) / Postgres(可选)
111
111
  | [storage.v0](docs/spec/storage.v0.md) | 路径、身份、迁移、并发、导出格式 |
112
112
  | [sync.v0](docs/spec/sync.v0.md) | 跨设备合并语义(v0.5 实现) |
113
113
  | [transcript.v1](docs/spec/transcript.v1.md) | 可移植会话转写格式(ingest 的输入) |
114
+ | [适配器索引](adapters/README.md) | 各宿主适配器与接入契约 |
114
115
  | [quality](docs/quality.md) | 质量模型、九条门禁、五条评审清单 |
115
116
  | [references](docs/references.md) | 外部资料的去留与过期判断 |
116
117
  | [runbook](docs/runbook.md) | 发版流程、维护操作、已知待办 |
@@ -0,0 +1,47 @@
1
+ # 宿主适配器
2
+
3
+ soulbridge 的核心不知道任何 agent 宿主的存在([ADR 0024](../adr/0024-adapters.md))。
4
+ 把四个动词接到具体宿主上的代码,都在这里列出的独立包里。
5
+
6
+ ## 现有适配器
7
+
8
+ | 宿主 | 阶梯 | 形态 | 位置 |
9
+ |---|---|---|---|
10
+ | DSH | T0 | `SKILL.md`(模型自觉调用 CLI) | [dsh/SKILL.md](dsh/SKILL.md) |
11
+ | DSH | T2 | cordis 插件(系统提示注入 + 事件观察) | [dsh-plugin-soulbridge](https://github.com/Jason0826/dsh-plugin-soulbridge) |
12
+
13
+ > T1(会话转写摄取)与宿主无关:任何宿主导出 `transcript.v1.jsonl` 后,用
14
+ > `soulbridge ingest --file` 即可,见 [spec/transcript.v1.md](../spec/transcript.v1.md)。
15
+
16
+ ## 适配器要做什么
17
+
18
+ | 时机 | 调用 | 失败时 |
19
+ |---|---|---|
20
+ | 提示组装 | `context({ track: false })` | 不注入 |
21
+ | 每条用户消息 | `observe(turn)` | 跳过 |
22
+ | 会话结束 | `reflect(turns)` | 丢弃 |
23
+ | 模型主动查询(可选) | `recall` / `remember` | 返回空 |
24
+
25
+ ## 三条禁止
26
+
27
+ 1. 不改变事实判断——affect 只影响措辞
28
+ 2. 不编造记忆——没注入的就是没有
29
+ 3. 不把个人记忆写进项目文件
30
+
31
+ ## 自己写一个适配器
32
+
33
+ 只需要 `soulbridge` 包暴露的:(TODO: 补 `soulbridge` 的 SDK 入口说明)
34
+
35
+ ```ts
36
+ import { openSqlite, ensureConfig, resolveDbPath, Soul } from 'soulbridge';
37
+
38
+ const config = ensureConfig();
39
+ const soul = new Soul({
40
+ store: openSqlite(resolveDbPath()),
41
+ soulId: config.soulId,
42
+ userId: config.userId,
43
+ deviceId: config.deviceId,
44
+ });
45
+ ```
46
+
47
+ 然后按上表在宿主的对应时机调用四个方法即可。
@@ -24,7 +24,7 @@ process.on('warning', (warning) => {
24
24
  return;
25
25
  process.stderr.write(`${warning.name}: ${warning.message}\n`);
26
26
  });
27
- const { openSqlite, Soul, ensureConfig, resolveDbPath, analyzeAffect, validateLexicon, parseSteering, normalizeForDedupe, parseTranscriptJsonl, } = await import('../../core/src/index.js');
27
+ const { openSqlite, Soul, ensureConfig, resolveDbPath, analyzeAffect, validateLexicon, parseSteering, normalizeForDedupe, parseTranscriptJsonl, soulbridgeHome, } = await import('../../core/src/index.js');
28
28
  const { llmAnalyzerFromEnv } = await import('./llm-analyzer.js');
29
29
  function parse(argv) {
30
30
  const [cmd = 'help', ...rest] = argv;
@@ -303,6 +303,71 @@ async function main() {
303
303
  out({ ok: true, file: resolve(file), turns: turns.length, skipped, reflected, dryRun });
304
304
  return;
305
305
  }
306
+ case 'list': {
307
+ const { soul } = open(flags);
308
+ const memories = soul.list(flags.limit ? Number(flags.limit) : 20);
309
+ if (memories.length === 0) {
310
+ process.stdout.write('no memories yet\n');
311
+ return;
312
+ }
313
+ for (const memory of memories) {
314
+ const preview = memory.content.replace(/\s+/g, ' ').slice(0, 72);
315
+ process.stdout.write(`${memory.id} ${memory.type.padEnd(9)} imp=${String(memory.importance).padEnd(2)} ${preview}\n`);
316
+ }
317
+ return;
318
+ }
319
+ case 'forget': {
320
+ const id = positional[0];
321
+ if (!id)
322
+ throw new Error('usage: soulbridge forget <memory-id>');
323
+ const { soul } = open(flags);
324
+ out({ ok: soul.forget(id), id });
325
+ return;
326
+ }
327
+ case 'status': {
328
+ const config = ensureConfig();
329
+ const dbPath = resolveDbPath(flags.db);
330
+ const store = openSqlite(dbPath);
331
+ const memories = store.query({ soulId: config.soulId, userId: config.userId, limit: 100_000 });
332
+ const samples = store.recentAffect(config.soulId, config.userId, 0);
333
+ store.close();
334
+ const byType = new Map();
335
+ for (const memory of memories)
336
+ byType.set(memory.type, (byType.get(memory.type) ?? 0) + 1);
337
+ const typeText = [...byType].sort((a, b) => b[1] - a[1]).map(([t, n]) => `${t} ${n}`).join(' · ') || '—';
338
+ let plugin = null;
339
+ try {
340
+ plugin = JSON.parse(readFileSync(resolve(soulbridgeHome(), 'plugin-state.json'), 'utf8'));
341
+ }
342
+ catch {
343
+ plugin = null;
344
+ }
345
+ const ago = (value) => {
346
+ if (typeof value !== 'number' || value <= 0)
347
+ return '—';
348
+ const seconds = Math.max(0, Math.round((Date.now() - value) / 1000));
349
+ if (seconds < 60)
350
+ return `${seconds}s ago`;
351
+ if (seconds < 3600)
352
+ return `${Math.round(seconds / 60)}m ago`;
353
+ if (seconds < 86_400)
354
+ return `${Math.round(seconds / 3600)}h ago`;
355
+ return `${Math.round(seconds / 86_400)}d ago`;
356
+ };
357
+ const lines = [
358
+ `soul ${config.soulId} / ${config.userId}`,
359
+ `db ${dbPath}`,
360
+ `memories ${memories.length} (${typeText})`,
361
+ `affect ${samples.length} samples${samples.length > 0 ? ` · last ${ago(samples[0].at)}` : ''}`,
362
+ plugin
363
+ ? `plugin v${plugin.version ?? '?'} · applied ${ago(plugin.appliedAt)} · inject ${plugin.injects ?? 0} · observe ${plugin.observes ?? 0} · fail ${plugin.observeFails ?? 0}`
364
+ : 'plugin no heartbeat (plugin never loaded, or runs on another machine)',
365
+ ];
366
+ if (plugin?.lastError)
367
+ lines.push(`last error ${String(plugin.lastError)}`);
368
+ process.stdout.write(`${lines.join('\n')}\n`);
369
+ return;
370
+ }
306
371
  case 'stats': {
307
372
  const { soul, dbPath } = open(flags);
308
373
  out({ db: dbPath, ...soul.stats() });
@@ -327,7 +392,10 @@ async function main() {
327
392
  ' import --in <file> 导入 JSONL',
328
393
  ' import --from <steering.md> 导入 AGENTS.md / CLAUDE.md',
329
394
  ' ingest --file <transcript.jsonl> 会话转写 → 记忆与情绪采样',
330
- ' stats 看状态',
395
+ ' list [--limit N] 看它记住了什么',
396
+ ' forget <id> 忘掉一条记忆(墓碑)',
397
+ ' status 一眼看它是否在工作',
398
+ ' stats 看原始状态(JSON)',
331
399
  ' demo 端到端演示(无需模型)',
332
400
  '',
333
401
  'env: SOULBRIDGE_DB, SOULBRIDGE_SOUL, SOULBRIDGE_USER, SOULBRIDGE_NAME,',
@@ -53,13 +53,21 @@ export function buildContext(store, request, now = Date.now()) {
53
53
  }
54
54
  }
55
55
  }
56
- const candidates = store.query({ soulId: request.soulId, userId: request.userId });
56
+ const candidates = store
57
+ .query({ soulId: request.soulId, userId: request.userId })
58
+ .filter((memory) => (request.types === undefined || request.types.includes(memory.type)) &&
59
+ (request.minImportance === undefined || memory.importance >= request.minImportance));
57
60
  const ranked = rank(candidates, request.query ?? '', now);
61
+ const maxItems = request.maxItems ?? Number.POSITIVE_INFINITY;
58
62
  let chars = sections.join('\n\n').length;
59
63
  let dropped = 0;
60
64
  const lines = [];
61
65
  const used = [];
62
66
  for (const { memory } of ranked) {
67
+ if (lines.length >= maxItems) {
68
+ dropped += 1;
69
+ continue;
70
+ }
63
71
  const line = `- [${memory.type}] ${memory.content}`;
64
72
  const cost = line.length + 1 + 10; // line + newline + section overhead share
65
73
  if (chars + cost > maxChars) {
@@ -73,7 +81,7 @@ export function buildContext(store, request, now = Date.now()) {
73
81
  if (lines.length > 0)
74
82
  sections.push(`<memories>\n${lines.join('\n')}\n</memories>`);
75
83
  const text = sections.join('\n\n');
76
- if (used.length > 0)
84
+ if (request.track !== false && used.length > 0)
77
85
  store.touch(used, now);
78
86
  const result = { text, used, dropped, chars: text.length };
79
87
  if (affectMeta)
@@ -17,7 +17,7 @@ export * from './storage/config.ts';
17
17
  export * from './storage/merge.ts';
18
18
  export * from './storage/steering.ts';
19
19
  export * from './storage/transcript.ts';
20
- import type { AffectSignal, ContextRequest, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, Visibility } from './types.ts';
20
+ import type { AffectSignal, ContextRequest, Memory, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, Visibility } from './types.ts';
21
21
  import type { SoulStore } from './store.ts';
22
22
  import type { AffectAnalyzer } from './affect/port.ts';
23
23
  export interface SoulOptions {
@@ -42,6 +42,10 @@ export declare class Soul {
42
42
  constructor(options: SoulOptions);
43
43
  /** Verb 1: session start. */
44
44
  context(request?: Partial<ContextRequest>): ContextResult;
45
+ /** Every active memory, newest first. For `list` and for auditing injection. */
46
+ list(limit?: number): Memory[];
47
+ /** Tombstone one memory so it never reaches a prompt again. */
48
+ forget(id: string): boolean;
45
49
  /** Verb 3: on demand. */
46
50
  recall(query: string, limit?: number): ScoredMemory[];
47
51
  /** Pure analysis, no storage. Async because the analyzer may call a model. */
@@ -48,8 +48,20 @@ export class Soul {
48
48
  userId: this.userId,
49
49
  query: request.query,
50
50
  maxChars: request.maxChars,
51
+ track: request.track,
52
+ types: request.types,
53
+ minImportance: request.minImportance,
54
+ maxItems: request.maxItems,
51
55
  });
52
56
  }
57
+ /** Every active memory, newest first. For `list` and for auditing injection. */
58
+ list(limit = 50) {
59
+ return this.store.query({ soulId: this.soulId, userId: this.userId, limit });
60
+ }
61
+ /** Tombstone one memory so it never reaches a prompt again. */
62
+ forget(id) {
63
+ return this.store.forget(id);
64
+ }
53
65
  /** Verb 3: on demand. */
54
66
  recall(query, limit = 5) {
55
67
  const memories = this.store.query({ soulId: this.soulId, userId: this.userId });
@@ -5,6 +5,27 @@ interface Draft {
5
5
  content: string;
6
6
  importance: number;
7
7
  }
8
+ interface Rule {
9
+ /** Stable id for tests and diagnostics. */
10
+ id: string;
11
+ type: MemoryType;
12
+ importance: number;
13
+ match: (text: string) => boolean;
14
+ /** Optional rewrite: keep the instruction, drop the surrounding discussion. */
15
+ distill?: (text: string) => string | null;
16
+ }
17
+ /** Long strategies are noise in a prompt; keep the instruction, not the essay. */
18
+ export declare const MAX_STRATEGY_CHARS = 80;
19
+ /**
20
+ * Cut a standing instruction down to the clause that carries it.
21
+ *
22
+ * A user explains for two lines and states the rule once; storing the whole
23
+ * paragraph repeats the discussion into every future prompt. Find the trigger
24
+ * whose neighbourhood also carries a request, and keep from there.
25
+ */
26
+ export declare function distillStrategy(text: string): string | null;
27
+ /** Cheap, synchronous, no model. Deliberately conservative: false positives cost trust. */
28
+ export declare const RULES: Rule[];
8
29
  export declare function ruleExtract(turns: Turn[]): Draft[];
9
30
  /** Corrections raise tension and lower passion; an ordinary session lets tension decay. */
10
31
  export declare function nextEmotion(current: Emotion, turns: Turn[]): Emotion;
@@ -7,12 +7,72 @@
7
7
  */
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { tokenize } from './scoring.js';
10
+ /**
11
+ * A standing instruction needs both halves: when it applies, and what it asks
12
+ * for. A bare "每次" is usually a complaint ("每次都不能用"), so the trigger
13
+ * alone must not create a strategy.
14
+ */
15
+ const TIME_TRIGGER = /(以后|下次|每次|从现在起|在.{0,12}之前|from now on|always)/i;
16
+ const DIRECTIVE = /(先|需要|要|必须|应该|请|别|不要|务必|记得|need to|should|must|please)/i;
17
+ /** Long strategies are noise in a prompt; keep the instruction, not the essay. */
18
+ export const MAX_STRATEGY_CHARS = 80;
19
+ /** How far after a trigger the request may sit and still belong to it. */
20
+ const DIRECTIVE_WINDOW = 40;
21
+ /**
22
+ * Cut a standing instruction down to the clause that carries it.
23
+ *
24
+ * A user explains for two lines and states the rule once; storing the whole
25
+ * paragraph repeats the discussion into every future prompt. Find the trigger
26
+ * whose neighbourhood also carries a request, and keep from there.
27
+ */
28
+ export function distillStrategy(text) {
29
+ const trigger = new RegExp(TIME_TRIGGER.source, 'gi');
30
+ let match = trigger.exec(text);
31
+ let start = -1;
32
+ // The rule usually closes the paragraph ("...比如你每次…都需要…"), so the
33
+ // last qualifying trigger is the instruction, not the setup.
34
+ while (match !== null) {
35
+ const rest = text.slice(match.index);
36
+ if (DIRECTIVE.test(rest.slice(0, DIRECTIVE_WINDOW)))
37
+ start = match.index;
38
+ match = trigger.exec(text);
39
+ }
40
+ if (start < 0)
41
+ return null;
42
+ let clause = text.slice(start).replace(/^[\s,,、::]+/, '');
43
+ clause = clause.replace(/^(比如|例如|就像|也就是说|你|我)\s*/g, '');
44
+ if (clause.length > MAX_STRATEGY_CHARS)
45
+ clause = clause.slice(0, MAX_STRATEGY_CHARS);
46
+ clause = clause.trim();
47
+ return clause.length > 0 ? clause : null;
48
+ }
10
49
  /** Cheap, synchronous, no model. Deliberately conservative: false positives cost trust. */
11
- const RULES = [
12
- { re: /(记住|别忘了|remember|don'?t forget)/i, type: 'fact', importance: 7 },
13
- { re: /(我喜欢|我习惯|我偏好|我倾向|i prefer|i like)/i, type: 'trait', importance: 6 },
14
- { re: /(以后|下次|每次|从现在起|from now on|always)/i, type: 'strategy', importance: 7 },
15
- { re: /(我决定|就这么定|原则是|i decided|my rule)/i, type: 'cognition', importance: 7 },
50
+ export const RULES = [
51
+ {
52
+ id: 'remember',
53
+ type: 'fact',
54
+ importance: 7,
55
+ match: (text) => /(记住|别忘了|remember|don'?t forget)/i.test(text),
56
+ },
57
+ {
58
+ id: 'preference',
59
+ type: 'trait',
60
+ importance: 6,
61
+ match: (text) => /(我喜欢|我习惯|我偏好|我倾向|i prefer|i like)/i.test(text),
62
+ },
63
+ {
64
+ id: 'standing-instruction',
65
+ type: 'strategy',
66
+ importance: 7,
67
+ match: (text) => TIME_TRIGGER.test(text) && DIRECTIVE.test(text),
68
+ distill: distillStrategy,
69
+ },
70
+ {
71
+ id: 'decision',
72
+ type: 'cognition',
73
+ importance: 7,
74
+ match: (text) => /(我决定|就这么定|原则是|i decided|my rule)/i.test(text),
75
+ },
16
76
  ];
17
77
  const CORRECTION = /(不对|不是这样|错了|你搞错|wrong|that'?s not right|incorrect)/i;
18
78
  const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
@@ -23,10 +83,13 @@ export function ruleExtract(turns) {
23
83
  if (turn.role !== 'user')
24
84
  continue;
25
85
  for (const rule of RULES) {
26
- if (rule.re.test(turn.content)) {
27
- drafts.push({ type: rule.type, content: turn.content.trim(), importance: rule.importance });
28
- break;
29
- }
86
+ if (!rule.match(turn.content))
87
+ continue;
88
+ // A rule may rewrite the turn; a strategy we cannot phrase is not stored.
89
+ const content = rule.distill ? rule.distill(turn.content) : turn.content.trim();
90
+ if (content)
91
+ drafts.push({ type: rule.type, content, importance: rule.importance });
92
+ break;
30
93
  }
31
94
  }
32
95
  return drafts;
@@ -225,6 +225,12 @@ export function openSqlite(file) {
225
225
  stmt.run(now, id);
226
226
  });
227
227
  },
228
+ forget(id, now = Date.now()) {
229
+ const result = withRetry(() => db
230
+ .prepare('UPDATE memories SET deleted_at = ?, updated_at = ? WHERE id = ? AND deleted_at IS NULL')
231
+ .run(now, now, id));
232
+ return Number(result.changes ?? 0) > 0;
233
+ },
228
234
  saveAffect(soulId, userId, signal, at, deviceId) {
229
235
  withRetry(() => {
230
236
  db.prepare(`INSERT OR IGNORE INTO affect_samples
@@ -13,6 +13,11 @@ export interface SoulStore {
13
13
  query(spec: QuerySpec): Memory[];
14
14
  /** Recall bookkeeping: recallCount++, lastRecalledAt = now. */
15
15
  touch(ids: string[], now?: number): void;
16
+ /**
17
+ * Tombstone a memory so it never surfaces again (sync-safe: the row stays for
18
+ * merge, the deletion wins by updatedAt). Returns false when no such active row.
19
+ */
20
+ forget(id: string, now?: number): boolean;
16
21
  /** Persist one affect sample (payload only; no raw text). */
17
22
  saveAffect(soulId: string, userId: string, signal: AffectSignal, at: number, deviceId?: string): void;
18
23
  /** Samples newer than `sinceMs`, newest first. */
@@ -57,6 +57,18 @@ export interface ContextRequest {
57
57
  userId: string;
58
58
  query?: string;
59
59
  maxChars?: number;
60
+ /**
61
+ * Whether this call counts as a recall (increments recallCount / lastRecalledAt).
62
+ * Host adapters assemble prompts repeatedly per turn; pass false to preview
63
+ * context without inflating recall statistics. Defaults to true.
64
+ */
65
+ track?: boolean;
66
+ /** Only these memory types may be injected. Defaults to every type. */
67
+ types?: MemoryType[];
68
+ /** Skip memories below this importance (1..10). */
69
+ minImportance?: number;
70
+ /** Cap the number of injected memory lines. */
71
+ maxItems?: number;
60
72
  }
61
73
  export interface ContextResult {
62
74
  text: string;
package/package.json CHANGED
@@ -1,59 +1,59 @@
1
1
  {
2
- "name": "soulbridge",
3
- "version": "0.0.6",
4
- "description": "A personality layer for any agent: continuity, forgetting, and growth across tools.",
5
- "license": "Apache-2.0",
6
- "type": "module",
7
- "main": "./dist/packages/core/src/index.js",
8
- "types": "./dist/packages/core/src/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/packages/core/src/index.d.ts",
12
- "import": "./dist/packages/core/src/index.js"
13
- },
14
- "./package.json": "./package.json"
15
- },
16
- "bin": {
17
- "soulbridge": "dist/packages/cli/src/cli.js"
18
- },
19
- "files": [
20
- "dist",
21
- "adapters",
22
- "README.md",
23
- "LICENSE",
24
- "NOTICE"
25
- ],
26
- "workspaces": [
27
- "packages/*"
28
- ],
29
- "engines": {
30
- "node": "\u003e=22.18"
31
- },
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/Jason0826/soulbridge.git"
35
- },
36
- "homepage": "https://github.com/Jason0826/soulbridge#readme",
37
- "bugs": {
38
- "url": "https://github.com/Jason0826/soulbridge/issues"
39
- },
40
- "keywords": [
41
- "ai-agent",
42
- "memory",
43
- "personality",
44
- "continuity",
45
- "mcp",
46
- "claude-code"
47
- ],
48
- "scripts": {
49
- "build": "tsc -p tsconfig.build.json",
50
- "test": "node --test \"packages/core/test/*.test.ts\"",
51
- "demo": "node packages/cli/src/cli.ts demo",
52
- "typecheck": "tsc --noEmit",
53
- "prepublishOnly": "npm run build \u0026\u0026 npm test"
54
- },
55
- "devDependencies": {
56
- "@types/node": "^26.6.5",
57
- "typescript": "^7.0.2"
58
- }
2
+ "name": "soulbridge",
3
+ "version": "0.0.8",
4
+ "description": "A personality layer for any agent: continuity, forgetting, and growth across tools.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/packages/core/src/index.js",
8
+ "types": "./dist/packages/core/src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/packages/core/src/index.d.ts",
12
+ "import": "./dist/packages/core/src/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "bin": {
17
+ "soulbridge": "dist/packages/cli/src/cli.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "adapters",
22
+ "README.md",
23
+ "LICENSE",
24
+ "NOTICE"
25
+ ],
26
+ "workspaces": [
27
+ "packages/*"
28
+ ],
29
+ "engines": {
30
+ "node": ">=22.18"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Jason0826/soulbridge.git"
35
+ },
36
+ "homepage": "https://github.com/Jason0826/soulbridge#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/Jason0826/soulbridge/issues"
39
+ },
40
+ "keywords": [
41
+ "ai-agent",
42
+ "memory",
43
+ "personality",
44
+ "continuity",
45
+ "mcp",
46
+ "claude-code"
47
+ ],
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "test": "node --test \"packages/core/test/*.test.ts\"",
51
+ "demo": "node packages/cli/src/cli.ts demo",
52
+ "typecheck": "tsc --noEmit",
53
+ "prepublishOnly": "npm run build && npm test"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^26.6.5",
57
+ "typescript": "^7.0.2"
58
+ }
59
59
  }