@dtoolkit/dproxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1398 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import chalk7 from "chalk";
5
+ import { Command as Command7 } from "commander";
6
+
7
+ // src/commands/ask.ts
8
+ import chalk from "chalk";
9
+ import { Command } from "commander";
10
+
11
+ // src/claude.ts
12
+ import { spawn } from "child_process";
13
+
14
+ // src/lib/config.ts
15
+ import { readFile, writeFile, rename, mkdir } from "fs/promises";
16
+ import { homedir } from "os";
17
+ import { join } from "path";
18
+ var DATA_DIR = process.env.DPROXY_DATA_DIR || join(homedir(), ".dproxy");
19
+ var DEFAULT_CONFIG = {
20
+ initialized: false,
21
+ memory: {
22
+ autoInject: true,
23
+ defaultKeys: [],
24
+ maxInjectionChars: 4e3
25
+ },
26
+ life: {
27
+ autoInject: true,
28
+ dir: "",
29
+ maxInjectionChars: 12e3,
30
+ maxEntityChars: 1500,
31
+ pythonBin: "python3"
32
+ },
33
+ history: {
34
+ maxEntries: 1e3
35
+ },
36
+ workspace: {
37
+ enabled: false,
38
+ dir: "",
39
+ maxInjectionChars: 16e3,
40
+ files: [
41
+ { file: "IDENTITY.md", header: "## Identity" },
42
+ { file: "SOUL.md", header: "## Soul / Personality" },
43
+ { file: "USER.md", header: "## User Profile" },
44
+ { file: "MEMORY.md", header: "## System Memory" }
45
+ ]
46
+ },
47
+ chatLog: {
48
+ enabled: false,
49
+ dir: "",
50
+ userPrefix: "User:",
51
+ assistantPrefix: "Assistant:",
52
+ sectionHeader: "## Today's conversation"
53
+ },
54
+ claude: {
55
+ bin: "claude",
56
+ skipPermissions: false
57
+ },
58
+ defaults: {},
59
+ debug: false
60
+ };
61
+ function getDataDir() {
62
+ return DATA_DIR;
63
+ }
64
+ async function ensureDataDir() {
65
+ await mkdir(DATA_DIR, { recursive: true });
66
+ await mkdir(join(DATA_DIR, "memory"), { recursive: true });
67
+ await mkdir(join(DATA_DIR, "templates"), { recursive: true });
68
+ }
69
+ function deepMerge(defaults, overrides) {
70
+ const result = { ...defaults };
71
+ for (const key in overrides) {
72
+ if (typeof defaults[key] === "object" && defaults[key] !== null && !Array.isArray(defaults[key]) && typeof overrides[key] === "object" && overrides[key] !== null && !Array.isArray(overrides[key])) {
73
+ result[key] = deepMerge(
74
+ defaults[key],
75
+ overrides[key]
76
+ );
77
+ } else {
78
+ result[key] = overrides[key];
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ async function loadConfig() {
84
+ try {
85
+ const raw = await readFile(join(DATA_DIR, "config.json"), "utf-8");
86
+ const parsed = JSON.parse(raw);
87
+ return deepMerge(
88
+ DEFAULT_CONFIG,
89
+ parsed
90
+ );
91
+ } catch {
92
+ return { ...DEFAULT_CONFIG };
93
+ }
94
+ }
95
+ async function saveConfig(config) {
96
+ await ensureDataDir();
97
+ await atomicWriteFile(join(DATA_DIR, "config.json"), JSON.stringify(config, null, 2) + "\n");
98
+ }
99
+ async function atomicWriteFile(filePath, data) {
100
+ const tmp = filePath + ".tmp";
101
+ await writeFile(tmp, data, "utf-8");
102
+ await rename(tmp, filePath);
103
+ }
104
+ async function getConfigValue(key) {
105
+ const config = await loadConfig();
106
+ const parts = key.split(".");
107
+ let current = config;
108
+ for (const part of parts) {
109
+ if (current && typeof current === "object" && part in current) {
110
+ current = current[part];
111
+ } else {
112
+ return void 0;
113
+ }
114
+ }
115
+ return current;
116
+ }
117
+ async function setConfigValue(key, value) {
118
+ const config = await loadConfig();
119
+ const parts = key.split(".");
120
+ let current = config;
121
+ for (let i = 0; i < parts.length - 1; i++) {
122
+ if (!(parts[i] in current) || typeof current[parts[i]] !== "object") {
123
+ current[parts[i]] = {};
124
+ }
125
+ current = current[parts[i]];
126
+ }
127
+ let parsed = value;
128
+ if (value === "true") parsed = true;
129
+ else if (value === "false") parsed = false;
130
+ else if (!isNaN(Number(value)) && value !== "") parsed = Number(value);
131
+ current[parts[parts.length - 1]] = parsed;
132
+ await saveConfig(config);
133
+ }
134
+
135
+ // src/claude.ts
136
+ async function execClaude(options) {
137
+ const config = await loadConfig();
138
+ const args = buildArgs(options, config.claude?.skipPermissions ?? false);
139
+ const bin = config.claude?.bin || "claude";
140
+ const startTime = Date.now();
141
+ return new Promise((resolve, reject) => {
142
+ const proc = spawn(bin, args, {
143
+ stdio: ["pipe", "pipe", "pipe"],
144
+ env: { ...process.env }
145
+ });
146
+ let stdout = "";
147
+ let stderr = "";
148
+ proc.stdout.on("data", (data) => {
149
+ stdout += data.toString();
150
+ });
151
+ proc.stderr.on("data", (data) => {
152
+ stderr += data.toString();
153
+ });
154
+ if (options.stdinContent) {
155
+ proc.stdin.write(options.stdinContent);
156
+ proc.stdin.end();
157
+ } else {
158
+ proc.stdin.end();
159
+ }
160
+ proc.on("error", (err) => {
161
+ if (err.code === "ENOENT") {
162
+ reject(
163
+ new Error("claude CLI not found. Make sure Claude Code is installed and on your PATH.")
164
+ );
165
+ } else {
166
+ reject(err);
167
+ }
168
+ });
169
+ proc.on("close", (code) => {
170
+ const durationMs = Date.now() - startTime;
171
+ if (code !== 0 && !stdout.trim()) {
172
+ reject(new Error(stderr || `claude exited with code ${code}`));
173
+ return;
174
+ }
175
+ try {
176
+ const parsed = JSON.parse(stdout);
177
+ const u = parsed.usage ?? {};
178
+ const usage = {
179
+ input: u.input_tokens ?? 0,
180
+ output: u.output_tokens ?? 0,
181
+ cacheWrite: u.cache_creation_input_tokens ?? 0,
182
+ cacheRead: u.cache_read_input_tokens ?? 0,
183
+ total: (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0)
184
+ };
185
+ resolve({
186
+ result: parsed.result ?? parsed.content ?? stdout,
187
+ sessionId: parsed.session_id ?? "",
188
+ costUsd: parsed.cost_usd ?? parsed.total_cost_usd ?? 0,
189
+ durationMs,
190
+ isError: parsed.is_error ?? false,
191
+ usage,
192
+ raw: parsed
193
+ });
194
+ } catch {
195
+ resolve({
196
+ result: stdout.trim(),
197
+ sessionId: "",
198
+ costUsd: 0,
199
+ durationMs,
200
+ isError: code !== 0
201
+ });
202
+ }
203
+ });
204
+ });
205
+ }
206
+ function buildArgs(options, skipPermissions) {
207
+ const args = ["--print", "--output-format", "json"];
208
+ if (skipPermissions) {
209
+ args.push("--dangerously-skip-permissions");
210
+ }
211
+ if (options.model) {
212
+ args.push("--model", options.model);
213
+ }
214
+ if (options.maxTurns !== void 0) {
215
+ args.push("--max-turns", String(options.maxTurns));
216
+ }
217
+ if (options.maxBudgetUsd !== void 0) {
218
+ args.push("--max-budget-usd", String(options.maxBudgetUsd));
219
+ }
220
+ if (options.systemPrompt) {
221
+ args.push("--system-prompt", options.systemPrompt);
222
+ }
223
+ if (options.appendSystemPrompt) {
224
+ args.push("--append-system-prompt", options.appendSystemPrompt);
225
+ }
226
+ if (options.resumeSessionId) {
227
+ args.push("--resume", options.resumeSessionId);
228
+ }
229
+ if (options.continueSession) {
230
+ args.push("--continue");
231
+ }
232
+ if (options.allowedTools) {
233
+ for (const tool of options.allowedTools) {
234
+ args.push("--allowedTools", tool);
235
+ }
236
+ }
237
+ if (options.additionalArgs) {
238
+ args.push(...options.additionalArgs);
239
+ }
240
+ args.push(options.prompt);
241
+ return args;
242
+ }
243
+ async function readStdin() {
244
+ if (process.stdin.isTTY) {
245
+ return "";
246
+ }
247
+ return new Promise((resolve) => {
248
+ let data = "";
249
+ const timeout = setTimeout(() => resolve(data), 5e3);
250
+ process.stdin.setEncoding("utf-8");
251
+ process.stdin.on("data", (chunk) => {
252
+ data += chunk;
253
+ });
254
+ process.stdin.on("end", () => {
255
+ clearTimeout(timeout);
256
+ resolve(data);
257
+ });
258
+ });
259
+ }
260
+
261
+ // src/lib/chat-log-store.ts
262
+ import { appendFile, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
263
+ import { join as join2 } from "path";
264
+ function getTodayFile(dir) {
265
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
266
+ return join2(dir, `${today}.md`);
267
+ }
268
+ async function buildDayChatContext() {
269
+ const config = await loadConfig();
270
+ const cl = config.chatLog;
271
+ if (!cl?.enabled || !cl?.dir) return "";
272
+ try {
273
+ const content = await readFile2(getTodayFile(cl.dir), "utf-8");
274
+ const trimmed = content.trim();
275
+ if (!trimmed) return "";
276
+ const header = cl.sectionHeader ?? "## Today's conversation";
277
+ return `${header}
278
+
279
+ ${trimmed}`;
280
+ } catch {
281
+ return "";
282
+ }
283
+ }
284
+ async function addChatLog(userPrompt, assistantResponse) {
285
+ const config = await loadConfig();
286
+ const cl = config.chatLog;
287
+ if (!cl?.enabled || !cl?.dir) return;
288
+ const userMsg = userPrompt.trim();
289
+ const assistantMsg = assistantResponse.trim();
290
+ if (!userMsg || !assistantMsg) return;
291
+ const userPrefix = cl.userPrefix ?? "User:";
292
+ const assistantPrefix = cl.assistantPrefix ?? "Assistant:";
293
+ await mkdir2(cl.dir, { recursive: true });
294
+ const entry = `${userPrefix} ${userMsg}
295
+ ${assistantPrefix} ${assistantMsg}
296
+
297
+ `;
298
+ await appendFile(getTodayFile(cl.dir), entry, "utf-8");
299
+ }
300
+
301
+ // src/lib/life-store.ts
302
+ import { execFile } from "child_process";
303
+ import { readFile as readFile3, readdir, stat, appendFile as appendFile2 } from "fs/promises";
304
+ import { join as join3, dirname } from "path";
305
+ import { fileURLToPath } from "url";
306
+ import { promisify } from "util";
307
+ var __dirname = dirname(fileURLToPath(import.meta.url));
308
+ var debugEnabled = null;
309
+ async function debugLog(msg) {
310
+ if (debugEnabled === null) {
311
+ const config = await loadConfig();
312
+ debugEnabled = config.debug ?? false;
313
+ }
314
+ if (!debugEnabled) return;
315
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
316
+ `;
317
+ appendFile2(join3(getDataDir(), "debug.log"), line).catch(() => {
318
+ });
319
+ }
320
+ var execFileAsync = promisify(execFile);
321
+ var SEARCH_SCRIPT = process.env.LIFE_SEARCH_BIN || join3(__dirname, "scripts", "search_facts.py");
322
+ async function searchRelevantEntities(query, lifeDir, limit = 10) {
323
+ const config = await loadConfig();
324
+ const pythonBin = config.life.pythonBin || "python3";
325
+ const { stdout } = await execFileAsync(pythonBin, [
326
+ SEARCH_SCRIPT,
327
+ query,
328
+ "--json",
329
+ "--limit",
330
+ String(limit),
331
+ "--life-dir",
332
+ lifeDir
333
+ ]);
334
+ const results = JSON.parse(stdout);
335
+ const seen = /* @__PURE__ */ new Set();
336
+ const entities = [];
337
+ for (const r of results) {
338
+ if (!seen.has(r.entity)) {
339
+ seen.add(r.entity);
340
+ entities.push(r.entity);
341
+ }
342
+ }
343
+ return { entities, facts: results };
344
+ }
345
+ async function discoverEntities(lifeDir) {
346
+ const entities = [];
347
+ async function scanDir(dir, collection) {
348
+ let entries;
349
+ try {
350
+ entries = await readdir(dir);
351
+ } catch {
352
+ return;
353
+ }
354
+ for (const entry of entries) {
355
+ const fullPath = join3(dir, entry);
356
+ const summaryPath = join3(fullPath, "summary.md");
357
+ try {
358
+ await stat(summaryPath);
359
+ entities.push({
360
+ name: entry,
361
+ collection,
362
+ summaryPath
363
+ });
364
+ } catch {
365
+ try {
366
+ const s = await stat(fullPath);
367
+ if (s.isDirectory()) {
368
+ await scanDir(fullPath, `${collection}/${entry}`);
369
+ }
370
+ } catch (err) {
371
+ await debugLog(`scanDir: skipping ${fullPath}: ${err}`);
372
+ }
373
+ }
374
+ }
375
+ }
376
+ for (const topLevel of ["projects", "areas", "resources"]) {
377
+ await scanDir(join3(lifeDir, topLevel), topLevel);
378
+ }
379
+ return entities;
380
+ }
381
+ async function buildLifeContext(query, maxChars) {
382
+ const config = await loadConfig();
383
+ const lifeDir = config.life.dir;
384
+ if (!lifeDir) return "";
385
+ const limit = maxChars ?? config.life.maxInjectionChars;
386
+ await debugLog(
387
+ `buildLifeContext called \u2014 lifeDir: ${lifeDir}, query: ${query?.slice(0, 80)}, limit: ${limit}`
388
+ );
389
+ if (query) {
390
+ try {
391
+ return await buildSearchedLifeContext(query, lifeDir, limit);
392
+ } catch (err) {
393
+ await debugLog(`PARA search failed, falling back to full scan: ${err}`);
394
+ }
395
+ } else {
396
+ await debugLog(`No query provided, skipping search`);
397
+ }
398
+ try {
399
+ const result = await buildFullLifeContext(lifeDir, limit);
400
+ await debugLog(`PARA full scan returned ${result.length} chars from dir: ${lifeDir}`);
401
+ return result;
402
+ } catch (err) {
403
+ await debugLog(`PARA full scan failed for dir ${lifeDir}: ${err}`);
404
+ return "";
405
+ }
406
+ }
407
+ async function findOwnerEntity(allEntities) {
408
+ for (const entity of allEntities) {
409
+ const itemsPath = join3(dirname(entity.summaryPath), "items.json");
410
+ try {
411
+ const raw = await readFile3(itemsPath, "utf-8");
412
+ const data = JSON.parse(raw);
413
+ if (data.category === "owner") return entity.name;
414
+ } catch {
415
+ }
416
+ }
417
+ return null;
418
+ }
419
+ async function buildSearchedLifeContext(query, lifeDir, limit) {
420
+ const { entities: relevantNames, facts } = await searchRelevantEntities(query, lifeDir);
421
+ await debugLog(`PARA search returned ${relevantNames.length} entities, ${facts.length} facts`);
422
+ const allEntities = await discoverEntities(lifeDir);
423
+ const entityMap = /* @__PURE__ */ new Map();
424
+ for (const e of allEntities) {
425
+ entityMap.set(e.name, e);
426
+ }
427
+ const ownerName = await findOwnerEntity(allEntities);
428
+ if (ownerName && !relevantNames.includes(ownerName)) {
429
+ relevantNames.unshift(ownerName);
430
+ await debugLog(`PARA injected owner entity: ${ownerName}`);
431
+ }
432
+ if (relevantNames.length === 0) return "";
433
+ const config = await loadConfig();
434
+ const maxEntityChars = config.life.maxEntityChars ?? 1500;
435
+ const parts = [];
436
+ let totalChars = 0;
437
+ for (const name of relevantNames) {
438
+ const entity = entityMap.get(name);
439
+ if (!entity) continue;
440
+ try {
441
+ const content = await readFile3(entity.summaryPath, "utf-8");
442
+ const trimmed = content.length > maxEntityChars ? content.slice(0, maxEntityChars) + "\n...(truncated)" : content;
443
+ const section = `## [${entity.collection}] ${entity.name}
444
+ ${trimmed}`;
445
+ if (totalChars + section.length > limit) break;
446
+ parts.push(section);
447
+ totalChars += section.length;
448
+ } catch (err) {
449
+ await debugLog(`buildSearchedLifeContext: failed to read ${entity.summaryPath}: ${err}`);
450
+ }
451
+ }
452
+ const topFacts = facts.slice(0, 15);
453
+ if (topFacts.length > 0) {
454
+ const factsSection = `## Relevant Facts
455
+ ${topFacts.map((f) => `- [${f.entity}] ${f.fact}`).join("\n")}`;
456
+ if (totalChars + factsSection.length <= limit) {
457
+ parts.push(factsSection);
458
+ }
459
+ }
460
+ await debugLog(`PARA searched context: ${parts.length} parts, ${totalChars} chars`);
461
+ if (parts.length === 0) return "";
462
+ return `You have the following life/PARA knowledge context relevant to this query:
463
+
464
+ ${parts.join("\n\n")}`;
465
+ }
466
+ async function buildFullLifeContext(lifeDir, limit) {
467
+ const config = await loadConfig();
468
+ const maxEntityChars = config.life.maxEntityChars ?? 1500;
469
+ const entities = await discoverEntities(lifeDir);
470
+ if (entities.length === 0) return "";
471
+ const parts = [];
472
+ let totalChars = 0;
473
+ try {
474
+ const index = await readFile3(join3(lifeDir, "index.md"), "utf-8");
475
+ const section = `## Life Overview
476
+ ${index}`;
477
+ if (section.length <= limit) {
478
+ parts.push(section);
479
+ totalChars += section.length;
480
+ }
481
+ } catch {
482
+ }
483
+ for (const entity of entities) {
484
+ try {
485
+ const content = await readFile3(entity.summaryPath, "utf-8");
486
+ const trimmed = content.length > maxEntityChars ? content.slice(0, maxEntityChars) + "\n...(truncated)" : content;
487
+ const section = `## [${entity.collection}] ${entity.name}
488
+ ${trimmed}`;
489
+ if (totalChars + section.length > limit) break;
490
+ parts.push(section);
491
+ totalChars += section.length;
492
+ } catch (err) {
493
+ await debugLog(`buildFullLifeContext: failed to read ${entity.summaryPath}: ${err}`);
494
+ }
495
+ }
496
+ if (parts.length === 0) return "";
497
+ return `You have the following life/PARA knowledge context about the user:
498
+
499
+ ${parts.join("\n\n")}`;
500
+ }
501
+
502
+ // src/lib/memory-store.ts
503
+ import { readFile as readFile4, writeFile as writeFile2, readdir as readdir2, unlink } from "fs/promises";
504
+ import { join as join4 } from "path";
505
+ function getMemoryDir() {
506
+ return join4(getDataDir(), "memory");
507
+ }
508
+ function slugify(key) {
509
+ return key.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
510
+ }
511
+ function keyToFile(key) {
512
+ return join4(getMemoryDir(), `${slugify(key)}.md`);
513
+ }
514
+ async function setMemory(key, value) {
515
+ await ensureDataDir();
516
+ await writeFile2(keyToFile(key), value, "utf-8");
517
+ }
518
+ async function getMemory(key) {
519
+ try {
520
+ return await readFile4(keyToFile(key), "utf-8");
521
+ } catch {
522
+ return null;
523
+ }
524
+ }
525
+ async function listMemoryKeys() {
526
+ try {
527
+ const files = await readdir2(getMemoryDir());
528
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
529
+ } catch {
530
+ return [];
531
+ }
532
+ }
533
+ async function searchMemory(query) {
534
+ const keys = await listMemoryKeys();
535
+ const lower = query.toLowerCase();
536
+ const results = [];
537
+ for (const key of keys) {
538
+ const content = await getMemory(key);
539
+ if (content && (key.toLowerCase().includes(lower) || content.toLowerCase().includes(lower))) {
540
+ results.push({ key, content });
541
+ }
542
+ }
543
+ return results;
544
+ }
545
+ async function deleteMemory(key) {
546
+ try {
547
+ await unlink(keyToFile(key));
548
+ return true;
549
+ } catch {
550
+ return false;
551
+ }
552
+ }
553
+ async function buildMemoryContext(keys, maxChars = 4e3) {
554
+ const allKeys = keys?.length ? keys : await listMemoryKeys();
555
+ if (allKeys.length === 0) return "";
556
+ const parts = [];
557
+ let totalChars = 0;
558
+ for (const key of allKeys) {
559
+ const content = await getMemory(key);
560
+ if (!content) continue;
561
+ const section = `## ${key}
562
+ ${content}`;
563
+ if (totalChars + section.length > maxChars) break;
564
+ parts.push(section);
565
+ totalChars += section.length;
566
+ }
567
+ if (parts.length === 0) return "";
568
+ return `You have the following memory/context notes:
569
+
570
+ ${parts.join("\n\n")}`;
571
+ }
572
+
573
+ // src/lib/workspace-store.ts
574
+ import { readFile as readFile5 } from "fs/promises";
575
+ import { join as join5 } from "path";
576
+ var DEFAULT_BOOTSTRAP_FILES = [
577
+ { file: "IDENTITY.md", header: "## Identity" },
578
+ { file: "SOUL.md", header: "## Soul / Personality" },
579
+ { file: "USER.md", header: "## User Profile" },
580
+ { file: "MEMORY.md", header: "## System Memory" }
581
+ ];
582
+ async function buildWorkspaceContext(maxChars) {
583
+ const config = await loadConfig();
584
+ const ws = config.workspace;
585
+ if (!ws?.enabled || !ws?.dir) return "";
586
+ const dir = ws.dir;
587
+ const files = ws.files?.length ? ws.files : DEFAULT_BOOTSTRAP_FILES;
588
+ const limit = maxChars ?? ws.maxInjectionChars ?? 16e3;
589
+ const parts = [];
590
+ let totalChars = 0;
591
+ for (const { file, header } of files) {
592
+ try {
593
+ const content = (await readFile5(join5(dir, file), "utf-8")).trim();
594
+ if (!content) continue;
595
+ const section = `${header}
596
+
597
+ ${content}`;
598
+ if (totalChars + section.length > limit) break;
599
+ parts.push(section);
600
+ totalChars += section.length;
601
+ } catch (err) {
602
+ if (err.code !== "ENOENT" && config.debug) {
603
+ console.error(`[dproxy debug] workspace file ${file} error:`, err);
604
+ }
605
+ }
606
+ }
607
+ if (parts.length === 0) return "";
608
+ return parts.join("\n\n---\n\n");
609
+ }
610
+
611
+ // src/lib/context-builder.ts
612
+ async function buildSystemPromptContext(opts, config) {
613
+ const parts = [];
614
+ let dayChatCtx = "";
615
+ if (opts.chatLog !== false) {
616
+ dayChatCtx = await buildDayChatContext();
617
+ }
618
+ if (opts.workspace !== false) {
619
+ const ctx = await buildWorkspaceContext();
620
+ if (ctx) parts.push(ctx);
621
+ }
622
+ if (opts.memory !== false) {
623
+ const keys = Array.isArray(opts.memory) ? opts.memory : config.memory.defaultKeys.length ? config.memory.defaultKeys : void 0;
624
+ const ctx = await buildMemoryContext(keys, config.memory.maxInjectionChars);
625
+ if (ctx) parts.push(ctx);
626
+ }
627
+ if (opts.life !== false && config.life.autoInject) {
628
+ const ctx = await buildLifeContext(opts.lifeQuery, config.life.maxInjectionChars);
629
+ if (ctx) parts.push(ctx);
630
+ }
631
+ if (dayChatCtx) {
632
+ parts.unshift(dayChatCtx);
633
+ }
634
+ if (parts.length === 0) return "";
635
+ return parts.join("\n\n---\n\n");
636
+ }
637
+
638
+ // src/lib/history-store.ts
639
+ import { randomUUID } from "crypto";
640
+ import { appendFile as appendFile3, readFile as readFile6 } from "fs/promises";
641
+ import { join as join6 } from "path";
642
+ function getHistoryPath() {
643
+ return join6(getDataDir(), "history.jsonl");
644
+ }
645
+ async function addHistoryEntry(entry) {
646
+ await ensureDataDir();
647
+ const full = {
648
+ id: randomUUID(),
649
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
650
+ ...entry
651
+ };
652
+ await appendFile3(getHistoryPath(), JSON.stringify(full) + "\n", "utf-8");
653
+ try {
654
+ const config = await loadConfig();
655
+ const maxEntries = config.history.maxEntries;
656
+ if (maxEntries > 0) {
657
+ const all = await readLines();
658
+ if (all.length > maxEntries) {
659
+ const kept = all.slice(-maxEntries);
660
+ await atomicWriteFile(
661
+ getHistoryPath(),
662
+ kept.map((e) => JSON.stringify(e)).join("\n") + "\n"
663
+ );
664
+ }
665
+ }
666
+ } catch (err) {
667
+ const cfg = await loadConfig();
668
+ if (cfg.debug) console.error("[dproxy debug] history pruning failed:", err);
669
+ }
670
+ return full;
671
+ }
672
+ async function listHistory(limit = 20) {
673
+ const lines = await readLines();
674
+ return lines.slice(-limit).reverse();
675
+ }
676
+ async function getHistoryEntry(id) {
677
+ const lines = await readLines();
678
+ return lines.find((e) => e.id === id || e.id.startsWith(id)) ?? null;
679
+ }
680
+ async function searchHistory(query) {
681
+ const lines = await readLines();
682
+ const lower = query.toLowerCase();
683
+ return lines.filter((e) => e.prompt.toLowerCase().includes(lower) || e.result.toLowerCase().includes(lower)).reverse();
684
+ }
685
+ async function clearHistory(before) {
686
+ const lines = await readLines();
687
+ if (!before) {
688
+ await atomicWriteFile(getHistoryPath(), "");
689
+ return lines.length;
690
+ }
691
+ const cutoff = new Date(before).getTime();
692
+ if (isNaN(cutoff)) throw new Error(`Invalid date: "${before}"`);
693
+ const kept = lines.filter((e) => new Date(e.timestamp).getTime() >= cutoff);
694
+ const removed = lines.length - kept.length;
695
+ await atomicWriteFile(
696
+ getHistoryPath(),
697
+ kept.map((e) => JSON.stringify(e)).join("\n") + (kept.length ? "\n" : "")
698
+ );
699
+ return removed;
700
+ }
701
+ async function readLines() {
702
+ try {
703
+ const content = await readFile6(getHistoryPath(), "utf-8");
704
+ return content.split("\n").filter((line) => line.trim()).flatMap((line) => {
705
+ try {
706
+ return [JSON.parse(line)];
707
+ } catch {
708
+ return [];
709
+ }
710
+ });
711
+ } catch {
712
+ return [];
713
+ }
714
+ }
715
+
716
+ // src/lib/session-state.ts
717
+ import { readFile as readFile7 } from "fs/promises";
718
+ import { join as join7 } from "path";
719
+ function getStateFile() {
720
+ return join7(getDataDir(), "session-state.json");
721
+ }
722
+ async function loadState() {
723
+ try {
724
+ const raw = await readFile7(getStateFile(), "utf-8");
725
+ return JSON.parse(raw);
726
+ } catch {
727
+ return {};
728
+ }
729
+ }
730
+ async function saveState(state) {
731
+ await atomicWriteFile(getStateFile(), JSON.stringify(state, null, 2));
732
+ }
733
+ async function getSessionTokens(sessionId) {
734
+ const state = await loadState();
735
+ return state[sessionId]?.totalTokens ?? 0;
736
+ }
737
+ async function updateSessionTokens(sessionId, totalTokens) {
738
+ const state = await loadState();
739
+ state[sessionId] = { totalTokens, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
740
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
741
+ const pruned = Object.fromEntries(
742
+ Object.entries(state).filter(([, s]) => new Date(s.updatedAt).getTime() >= cutoff)
743
+ );
744
+ await saveState(pruned);
745
+ }
746
+
747
+ // src/commands/ask.ts
748
+ function createAskCommand() {
749
+ return new Command("ask").description("Send a prompt to Claude").argument("[prompt...]", "The prompt to send").option("-m, --model <model>", "Model to use").option("--max-turns <n>", "Max agent turns", parseInt).option("--max-budget-usd <n>", "Max budget in USD", parseFloat).option("-o, --output-format <format>", "Output format: text, json, stream-json").option("--system-prompt <text>", "System prompt override").option("--no-memory", "Skip memory injection").option("--memory <keys>", "Inject only specific memory keys (comma-separated)").option("--no-life", "Skip life/PARA context injection").option("--no-history", "Don't save to history").option("--raw", "Print raw JSON response").option("--token-footer", "Append token usage footer to response text").option(
750
+ "--max-session-tokens <n>",
751
+ "Reset session if context exceeds this token count",
752
+ parseInt
753
+ ).option("-c, --continue", "Continue last conversation").option("-r, --resume <id>", "Resume a specific session").action(async (promptParts, opts) => {
754
+ try {
755
+ await runAsk(promptParts, opts);
756
+ } catch (err) {
757
+ console.error(chalk.red(err.message));
758
+ process.exit(1);
759
+ }
760
+ });
761
+ }
762
+ async function runAsk(promptParts, opts) {
763
+ const config = await loadConfig();
764
+ const stdinContent = await readStdin();
765
+ const promptText = promptParts.join(" ");
766
+ if (!promptText && !stdinContent) {
767
+ console.error(chalk.red('No prompt provided. Usage: dproxy ask "your question"'));
768
+ process.exit(1);
769
+ }
770
+ let fullPrompt = promptText;
771
+ if (stdinContent) {
772
+ fullPrompt = fullPrompt ? `${fullPrompt}
773
+
774
+ ---
775
+
776
+ ${stdinContent}` : stdinContent;
777
+ }
778
+ const memoryOpt = opts.memory === false ? false : typeof opts.memory === "string" ? opts.memory.split(",") : true;
779
+ const appendSystemPrompt = await buildSystemPromptContext(
780
+ {
781
+ memory: memoryOpt,
782
+ life: opts.life !== false,
783
+ workspace: true,
784
+ chatLog: true,
785
+ lifeQuery: fullPrompt
786
+ },
787
+ config
788
+ ) || void 0;
789
+ let resumeSessionId = opts.resume;
790
+ const maxSessionTokens = opts.maxSessionTokens;
791
+ if (resumeSessionId && maxSessionTokens) {
792
+ const currentTokens = await getSessionTokens(resumeSessionId);
793
+ if (currentTokens > maxSessionTokens) {
794
+ resumeSessionId = void 0;
795
+ }
796
+ }
797
+ const claudeOpts = {
798
+ prompt: fullPrompt,
799
+ model: opts.model ?? config.defaults.model,
800
+ maxTurns: opts.maxTurns ?? config.defaults.maxTurns,
801
+ maxBudgetUsd: opts.maxBudgetUsd,
802
+ systemPrompt: opts.systemPrompt,
803
+ appendSystemPrompt,
804
+ resumeSessionId,
805
+ continueSession: opts.continue
806
+ };
807
+ const result = await execClaude(claudeOpts);
808
+ const resultText = result.result;
809
+ if (result.sessionId && result.usage) {
810
+ await updateSessionTokens(result.sessionId, result.usage.total);
811
+ }
812
+ if (opts.tokenFooter && result.usage) {
813
+ const u = result.usage;
814
+ const parts = [];
815
+ if (u.cacheWrite > 0) parts.push(`cW:${u.cacheWrite.toLocaleString()}`);
816
+ if (u.cacheRead > 0) parts.push(`cR:${u.cacheRead.toLocaleString()}`);
817
+ parts.push(`in:${u.input.toLocaleString()}`);
818
+ parts.push(`out:${u.output.toLocaleString()}`);
819
+ parts.push(`~${u.total.toLocaleString()}`);
820
+ const footer = `
821
+
822
+ \u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014\u2014
823
+ \`${parts.join(" \xB7 ")}\``;
824
+ result.result += footer;
825
+ if (result.raw && typeof result.raw === "object") {
826
+ result.raw.result = result.result;
827
+ }
828
+ }
829
+ if (opts.raw) {
830
+ console.log(JSON.stringify(result.raw ?? result, null, 2));
831
+ } else if (opts.outputFormat === "json") {
832
+ console.log(
833
+ JSON.stringify(
834
+ { result: result.result, sessionId: result.sessionId, costUsd: result.costUsd },
835
+ null,
836
+ 2
837
+ )
838
+ );
839
+ } else {
840
+ console.log(result.result);
841
+ }
842
+ if (opts.history !== false) {
843
+ await addHistoryEntry({
844
+ prompt: fullPrompt,
845
+ result: result.result,
846
+ sessionId: result.sessionId,
847
+ costUsd: result.costUsd,
848
+ durationMs: result.durationMs,
849
+ model: opts.model ?? config.defaults.model
850
+ });
851
+ }
852
+ void addChatLog(promptText, resultText);
853
+ }
854
+
855
+ // src/commands/chat.ts
856
+ import { readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
857
+ import { join as join8 } from "path";
858
+ import { createInterface } from "readline";
859
+ import chalk2 from "chalk";
860
+ import { Command as Command2 } from "commander";
861
+ var SESSION_FILE = "current-session.json";
862
+ async function loadSession() {
863
+ try {
864
+ const raw = await readFile8(join8(getDataDir(), SESSION_FILE), "utf-8");
865
+ return JSON.parse(raw);
866
+ } catch {
867
+ return null;
868
+ }
869
+ }
870
+ async function saveSession(session) {
871
+ await ensureDataDir();
872
+ await writeFile3(
873
+ join8(getDataDir(), SESSION_FILE),
874
+ JSON.stringify(session, null, 2) + "\n",
875
+ "utf-8"
876
+ );
877
+ }
878
+ function createChatCommand() {
879
+ return new Command2("chat").description("Start an interactive conversation with Claude").option("-c, --continue", "Continue last conversation").option("-r, --resume <id>", "Resume a specific session").option("-m, --model <model>", "Model to use").option("--max-turns <n>", "Max agent turns per message", parseInt).option("--no-memory", "Skip memory injection").option("--no-life", "Skip life/PARA context injection").action(async (opts) => {
880
+ try {
881
+ await runChat(opts);
882
+ } catch (err) {
883
+ console.error(chalk2.red(err.message));
884
+ process.exit(1);
885
+ }
886
+ });
887
+ }
888
+ async function runChat(opts) {
889
+ const config = await loadConfig();
890
+ let sessionId;
891
+ if (opts.resume) {
892
+ sessionId = opts.resume;
893
+ console.log(chalk2.dim(`Resuming session: ${sessionId}`));
894
+ } else if (opts.continue) {
895
+ const prev = await loadSession();
896
+ if (prev) {
897
+ sessionId = prev.sessionId;
898
+ console.log(chalk2.dim(`Continuing session: ${sessionId}`));
899
+ } else {
900
+ console.log(chalk2.dim("No previous session found. Starting new chat."));
901
+ }
902
+ }
903
+ const appendSystemPrompt = await buildSystemPromptContext(
904
+ {
905
+ memory: opts.memory !== false,
906
+ life: opts.life !== false,
907
+ workspace: true,
908
+ chatLog: true
909
+ },
910
+ config
911
+ ) || void 0;
912
+ console.log(chalk2.bold.blue("Claude Chat"));
913
+ console.log(chalk2.dim('Type "exit" or Ctrl+C to quit.\n'));
914
+ const rl = createInterface({
915
+ input: process.stdin,
916
+ output: process.stdout,
917
+ prompt: chalk2.green("you > ")
918
+ });
919
+ rl.prompt();
920
+ rl.on("line", async (line) => {
921
+ const input = line.trim();
922
+ if (!input) {
923
+ rl.prompt();
924
+ return;
925
+ }
926
+ if (input === "exit" || input === "quit") {
927
+ console.log(chalk2.dim("Bye!"));
928
+ rl.close();
929
+ return;
930
+ }
931
+ rl.pause();
932
+ try {
933
+ process.stdout.write(chalk2.dim("thinking...\r"));
934
+ const result = await execClaude({
935
+ prompt: input,
936
+ model: opts.model ?? config.defaults.model,
937
+ maxTurns: opts.maxTurns ?? config.defaults.maxTurns,
938
+ appendSystemPrompt,
939
+ resumeSessionId: sessionId
940
+ });
941
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
942
+ console.log(chalk2.cyan("claude > ") + result.result);
943
+ console.log();
944
+ if (!sessionId && result.sessionId) {
945
+ sessionId = result.sessionId;
946
+ await saveSession({
947
+ sessionId,
948
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
949
+ });
950
+ }
951
+ await addHistoryEntry({
952
+ prompt: input,
953
+ result: result.result,
954
+ sessionId: result.sessionId,
955
+ costUsd: result.costUsd,
956
+ durationMs: result.durationMs,
957
+ model: opts.model ?? config.defaults.model
958
+ });
959
+ void addChatLog(input, result.result);
960
+ } catch (err) {
961
+ console.error(chalk2.red(err.message));
962
+ }
963
+ rl.resume();
964
+ rl.prompt();
965
+ });
966
+ rl.on("close", () => {
967
+ process.exit(0);
968
+ });
969
+ }
970
+
971
+ // src/commands/history.ts
972
+ import chalk3 from "chalk";
973
+ import { Command as Command3 } from "commander";
974
+ function printEntryList(entries, showCost = true) {
975
+ if (entries.length === 0) {
976
+ console.log(chalk3.dim("No history entries."));
977
+ return;
978
+ }
979
+ for (const e of entries) {
980
+ const date = new Date(e.timestamp).toLocaleString();
981
+ const prompt = e.prompt.length > 60 ? e.prompt.slice(0, 60) + "\u2026" : e.prompt;
982
+ const cost = showCost && e.costUsd ? chalk3.dim(`$${e.costUsd.toFixed(4)}`) : "";
983
+ console.log(`${chalk3.dim(e.id.slice(0, 8))} ${chalk3.blue(date)} ${prompt} ${cost}`);
984
+ }
985
+ }
986
+ function createHistoryCommand() {
987
+ const cmd = new Command3("history").description("Manage query history");
988
+ cmd.command("list").description("List recent queries").option("-l, --limit <n>", "Number of entries", parseInt, 20).action(async (opts) => {
989
+ const entries = await listHistory(opts.limit);
990
+ printEntryList(entries);
991
+ });
992
+ cmd.command("show <id>").description("Show a specific history entry").action(async (id) => {
993
+ const entry = await getHistoryEntry(id);
994
+ if (!entry) {
995
+ console.error(chalk3.red(`Entry not found: ${id}`));
996
+ process.exit(1);
997
+ }
998
+ console.log(chalk3.bold("Prompt:"));
999
+ console.log(entry.prompt);
1000
+ console.log();
1001
+ console.log(chalk3.bold("Response:"));
1002
+ console.log(entry.result);
1003
+ console.log();
1004
+ console.log(
1005
+ chalk3.dim(
1006
+ `Session: ${entry.sessionId || "n/a"} | Cost: $${entry.costUsd.toFixed(4)} | Duration: ${entry.durationMs}ms | ${entry.timestamp}`
1007
+ )
1008
+ );
1009
+ });
1010
+ cmd.command("search <query>").description("Search history").action(async (query) => {
1011
+ const entries = await searchHistory(query);
1012
+ printEntryList(entries, false);
1013
+ });
1014
+ cmd.command("clear").description("Clear history").option("--before <date>", "Clear entries before this date").action(async (opts) => {
1015
+ const removed = await clearHistory(opts.before);
1016
+ console.log(chalk3.green(`Cleared ${removed} entries.`));
1017
+ });
1018
+ cmd.action(async () => {
1019
+ const entries = await listHistory(20);
1020
+ printEntryList(entries);
1021
+ });
1022
+ return cmd;
1023
+ }
1024
+
1025
+ // src/commands/init.ts
1026
+ import { createInterface as createInterface2 } from "readline";
1027
+ import chalk4 from "chalk";
1028
+ import { Command as Command4 } from "commander";
1029
+ function ask(rl, question, def = "") {
1030
+ const suffix = def ? chalk4.dim(` (${def})`) : "";
1031
+ return new Promise((resolve) => {
1032
+ rl.question(`${question}${suffix}: `, (answer) => {
1033
+ resolve(answer.trim() || def);
1034
+ });
1035
+ });
1036
+ }
1037
+ function askBool(rl, question, def = false) {
1038
+ const hint = def ? "Y/n" : "y/N";
1039
+ return new Promise((resolve) => {
1040
+ rl.question(`${question} ${chalk4.dim(`[${hint}]`)}: `, (answer) => {
1041
+ const a = answer.trim().toLowerCase();
1042
+ if (!a) return resolve(def);
1043
+ resolve(a === "y" || a === "yes");
1044
+ });
1045
+ });
1046
+ }
1047
+ function createInitCommand() {
1048
+ return new Command4("init").description("Interactive setup wizard \u2014 required before first use").action(async () => {
1049
+ try {
1050
+ await runInit();
1051
+ } catch (err) {
1052
+ console.error(chalk4.red(err.message));
1053
+ process.exit(1);
1054
+ }
1055
+ });
1056
+ }
1057
+ async function runInit() {
1058
+ const config = await loadConfig();
1059
+ console.log();
1060
+ console.log(chalk4.bold.blue("dproxy \u2014 Setup"));
1061
+ console.log(chalk4.dim("Configure your environment. Press Enter to accept defaults.\n"));
1062
+ const rl = createInterface2({
1063
+ input: process.stdin,
1064
+ output: process.stdout
1065
+ });
1066
+ try {
1067
+ config.claude.bin = await ask(rl, "Claude binary path", config.claude.bin || "claude");
1068
+ const model = await ask(
1069
+ rl,
1070
+ "Default model (leave empty for CLI default)",
1071
+ config.defaults.model || ""
1072
+ );
1073
+ config.defaults.model = model || void 0;
1074
+ config.claude.skipPermissions = await askBool(
1075
+ rl,
1076
+ "Skip Claude permission prompts (--dangerously-skip-permissions)?",
1077
+ config.claude.skipPermissions
1078
+ );
1079
+ console.log();
1080
+ console.log(chalk4.bold("Optional integrations"));
1081
+ console.log(chalk4.dim("These inject extra context into every prompt.\n"));
1082
+ const enableWorkspace = await askBool(
1083
+ rl,
1084
+ "Enable workspace context (IDENTITY.md, SOUL.md, USER.md, MEMORY.md)?",
1085
+ config.workspace.enabled
1086
+ );
1087
+ config.workspace.enabled = enableWorkspace;
1088
+ if (enableWorkspace) {
1089
+ config.workspace.dir = await ask(rl, " Workspace directory", config.workspace.dir || "");
1090
+ }
1091
+ const enableChatLog = await askBool(rl, "Enable daily chat log?", config.chatLog.enabled);
1092
+ config.chatLog.enabled = enableChatLog;
1093
+ if (enableChatLog) {
1094
+ config.chatLog.dir = await ask(rl, " Chat log directory", config.chatLog.dir || "");
1095
+ }
1096
+ const lifeDir = await ask(
1097
+ rl,
1098
+ "PARA knowledge base directory (leave empty to skip)",
1099
+ config.life.dir || ""
1100
+ );
1101
+ config.life.dir = lifeDir;
1102
+ config.debug = await askBool(rl, "Enable debug logging?", config.debug);
1103
+ config.initialized = true;
1104
+ await saveConfig(config);
1105
+ console.log();
1106
+ console.log(chalk4.green("Configuration saved to ") + chalk4.dim(getDataDir() + "/config.json"));
1107
+ console.log(chalk4.green("You're ready to go! Try: ") + chalk4.bold('dproxy "hello"'));
1108
+ console.log();
1109
+ } finally {
1110
+ rl.close();
1111
+ }
1112
+ }
1113
+ async function requireInit() {
1114
+ const config = await loadConfig();
1115
+ if (config.initialized) return;
1116
+ console.error(
1117
+ chalk4.yellow("dproxy is not configured yet. Run ") + chalk4.bold("dproxy init") + chalk4.yellow(" to set up your environment.")
1118
+ );
1119
+ process.exit(1);
1120
+ }
1121
+
1122
+ // src/commands/memory.ts
1123
+ import chalk5 from "chalk";
1124
+ import { Command as Command5 } from "commander";
1125
+ function printKeyList(keys) {
1126
+ if (keys.length === 0) {
1127
+ console.log(chalk5.dim("No memory entries."));
1128
+ return;
1129
+ }
1130
+ for (const key of keys) {
1131
+ console.log(chalk5.blue(key));
1132
+ }
1133
+ }
1134
+ function createMemoryCommand() {
1135
+ const cmd = new Command5("memory").description("Manage persistent memory");
1136
+ cmd.command("set <key> <value...>").description("Set a memory entry").action(async (key, valueParts) => {
1137
+ const value = valueParts.join(" ");
1138
+ await setMemory(key, value);
1139
+ console.log(chalk5.green(`Memory "${key}" saved.`));
1140
+ });
1141
+ cmd.command("get <key>").description("Get a memory entry").action(async (key) => {
1142
+ const value = await getMemory(key);
1143
+ if (value === null) {
1144
+ console.error(chalk5.red(`Memory "${key}" not found.`));
1145
+ process.exit(1);
1146
+ }
1147
+ console.log(value);
1148
+ });
1149
+ cmd.command("list").description("List all memory keys").action(async () => {
1150
+ const keys = await listMemoryKeys();
1151
+ printKeyList(keys);
1152
+ });
1153
+ cmd.command("search <query>").description("Search memory entries").action(async (query) => {
1154
+ const results = await searchMemory(query);
1155
+ if (results.length === 0) {
1156
+ console.log(chalk5.dim("No matches."));
1157
+ return;
1158
+ }
1159
+ for (const { key, content } of results) {
1160
+ const preview = content.length > 80 ? content.slice(0, 80) + "\u2026" : content;
1161
+ console.log(`${chalk5.blue(key)}: ${preview}`);
1162
+ }
1163
+ });
1164
+ cmd.command("delete <key>").description("Delete a memory entry").action(async (key) => {
1165
+ const deleted = await deleteMemory(key);
1166
+ if (deleted) {
1167
+ console.log(chalk5.green(`Memory "${key}" deleted.`));
1168
+ } else {
1169
+ console.error(chalk5.red(`Memory "${key}" not found.`));
1170
+ }
1171
+ });
1172
+ cmd.action(async () => {
1173
+ const keys = await listMemoryKeys();
1174
+ printKeyList(keys);
1175
+ });
1176
+ return cmd;
1177
+ }
1178
+
1179
+ // src/commands/template.ts
1180
+ import { readFile as readFile10 } from "fs/promises";
1181
+ import chalk6 from "chalk";
1182
+ import { Command as Command6 } from "commander";
1183
+ import { parse as parse2 } from "yaml";
1184
+
1185
+ // src/lib/template-store.ts
1186
+ import { readFile as readFile9, writeFile as writeFile4, readdir as readdir3, unlink as unlink2 } from "fs/promises";
1187
+ import { join as join9 } from "path";
1188
+ import { parse, stringify } from "yaml";
1189
+ function getTemplateDir() {
1190
+ return join9(getDataDir(), "templates");
1191
+ }
1192
+ function nameToFile(name) {
1193
+ return join9(getTemplateDir(), `${name}.yaml`);
1194
+ }
1195
+ async function getTemplate(name) {
1196
+ try {
1197
+ const raw = await readFile9(nameToFile(name), "utf-8");
1198
+ return parse(raw);
1199
+ } catch {
1200
+ return null;
1201
+ }
1202
+ }
1203
+ async function saveTemplate(template) {
1204
+ await ensureDataDir();
1205
+ await writeFile4(nameToFile(template.name), stringify(template), "utf-8");
1206
+ }
1207
+ async function listTemplates() {
1208
+ try {
1209
+ const files = await readdir3(getTemplateDir());
1210
+ const templates = [];
1211
+ for (const f of files) {
1212
+ if (!f.endsWith(".yaml")) continue;
1213
+ const raw = await readFile9(join9(getTemplateDir(), f), "utf-8");
1214
+ templates.push(parse(raw));
1215
+ }
1216
+ return templates;
1217
+ } catch {
1218
+ return [];
1219
+ }
1220
+ }
1221
+ async function deleteTemplate(name) {
1222
+ try {
1223
+ await unlink2(nameToFile(name));
1224
+ return true;
1225
+ } catch {
1226
+ return false;
1227
+ }
1228
+ }
1229
+ function renderTemplate(template, vars) {
1230
+ let prompt = template.prompt;
1231
+ for (const [key, value] of Object.entries(vars)) {
1232
+ prompt = prompt.replaceAll(`{{${key}}}`, value);
1233
+ }
1234
+ const unresolved = prompt.match(/\{\{([^}]+)\}\}/g);
1235
+ if (unresolved) {
1236
+ const names = unresolved.map((v) => v.slice(2, -2));
1237
+ console.error(`Warning: unresolved template variable(s): ${names.join(", ")}`);
1238
+ prompt = prompt.replace(/\{\{[^}]+\}\}/g, "");
1239
+ }
1240
+ return prompt;
1241
+ }
1242
+
1243
+ // src/commands/template.ts
1244
+ function createTemplateCommand() {
1245
+ const cmd = new Command6("template").description("Manage prompt templates");
1246
+ cmd.command("list").description("List all templates").action(async () => {
1247
+ const templates = await listTemplates();
1248
+ if (templates.length === 0) {
1249
+ console.log(chalk6.dim("No templates. Use 'dproxy template add <name>' to create one."));
1250
+ return;
1251
+ }
1252
+ for (const t of templates) {
1253
+ const desc = t.description ? chalk6.dim(` \u2014 ${t.description}`) : "";
1254
+ console.log(`${chalk6.blue(t.name)}${desc}`);
1255
+ }
1256
+ });
1257
+ cmd.command("show <name>").description("Show template details").action(async (name) => {
1258
+ const t = await getTemplate(name);
1259
+ if (!t) {
1260
+ console.error(chalk6.red(`Template "${name}" not found.`));
1261
+ process.exit(1);
1262
+ }
1263
+ console.log(chalk6.bold(t.name));
1264
+ if (t.description) console.log(chalk6.dim(t.description));
1265
+ console.log();
1266
+ console.log(chalk6.bold("Prompt:"));
1267
+ console.log(t.prompt);
1268
+ if (t.variables?.length) {
1269
+ console.log();
1270
+ console.log(chalk6.bold("Variables:"));
1271
+ for (const v of t.variables) {
1272
+ const req = v.required ? chalk6.red("*") : "";
1273
+ const def = v.default ? chalk6.dim(` (default: ${v.default})`) : "";
1274
+ const src = v.source ? chalk6.dim(` [${v.source}]`) : "";
1275
+ console.log(` {{${v.name}}}${req}${def}${src}`);
1276
+ }
1277
+ }
1278
+ });
1279
+ cmd.command("add <name>").description("Add a template from a YAML file or interactively").option("-f, --file <path>", "Import from YAML file").option("-d, --description <text>", "Template description").option("-p, --prompt <text>", "Template prompt text").action(async (name, opts) => {
1280
+ let template;
1281
+ if (opts.file) {
1282
+ const raw = await readFile10(opts.file, "utf-8");
1283
+ template = parse2(raw);
1284
+ template.name = name;
1285
+ } else if (opts.prompt) {
1286
+ template = {
1287
+ name,
1288
+ description: opts.description,
1289
+ prompt: opts.prompt
1290
+ };
1291
+ } else {
1292
+ console.error(chalk6.red("Provide --file or --prompt"));
1293
+ process.exit(1);
1294
+ }
1295
+ await saveTemplate(template);
1296
+ console.log(chalk6.green(`Template "${name}" saved.`));
1297
+ });
1298
+ cmd.command("run <name>").description("Run a template").option("--var <pairs...>", "Variables as key=value pairs").option("-m, --model <model>", "Model override").option("--raw", "Print raw JSON response").action(async (name, opts) => {
1299
+ const t = await getTemplate(name);
1300
+ if (!t) {
1301
+ console.error(chalk6.red(`Template "${name}" not found.`));
1302
+ process.exit(1);
1303
+ }
1304
+ const vars = {};
1305
+ if (opts.var) {
1306
+ for (const pair of opts.var) {
1307
+ const eq = pair.indexOf("=");
1308
+ if (eq === -1) continue;
1309
+ vars[pair.slice(0, eq)] = pair.slice(eq + 1);
1310
+ }
1311
+ }
1312
+ const stdinVar = t.variables?.find((v) => v.source === "stdin");
1313
+ if (stdinVar) {
1314
+ const stdinContent = await readStdin();
1315
+ if (stdinContent) {
1316
+ vars[stdinVar.name] = stdinContent;
1317
+ }
1318
+ }
1319
+ if (t.variables) {
1320
+ for (const v of t.variables) {
1321
+ if (!(v.name in vars) && v.default) {
1322
+ vars[v.name] = v.default;
1323
+ }
1324
+ if (v.required && !(v.name in vars)) {
1325
+ console.error(chalk6.red(`Missing required variable: {{${v.name}}}`));
1326
+ process.exit(1);
1327
+ }
1328
+ }
1329
+ }
1330
+ const rendered = renderTemplate(t, vars);
1331
+ await runAsk([rendered], {
1332
+ model: opts.model ?? t.claudeOptions?.model,
1333
+ maxTurns: t.claudeOptions?.maxTurns,
1334
+ maxBudgetUsd: t.claudeOptions?.maxBudgetUsd,
1335
+ raw: opts.raw,
1336
+ memory: true,
1337
+ history: true,
1338
+ templateUsed: name
1339
+ });
1340
+ });
1341
+ cmd.command("delete <name>").description("Delete a template").action(async (name) => {
1342
+ const deleted = await deleteTemplate(name);
1343
+ if (deleted) {
1344
+ console.log(chalk6.green(`Template "${name}" deleted.`));
1345
+ } else {
1346
+ console.error(chalk6.red(`Template "${name}" not found.`));
1347
+ }
1348
+ });
1349
+ return cmd;
1350
+ }
1351
+
1352
+ // src/index.ts
1353
+ var program = new Command7();
1354
+ program.name("dproxy").description("Universal adapter for invoking models via local CLIs").version("0.1.0");
1355
+ program.addCommand(createInitCommand());
1356
+ var guarded = (cmd) => {
1357
+ cmd.hook("preAction", async () => {
1358
+ await requireInit();
1359
+ });
1360
+ return cmd;
1361
+ };
1362
+ program.addCommand(guarded(createAskCommand()));
1363
+ program.addCommand(guarded(createChatCommand()));
1364
+ program.addCommand(guarded(createHistoryCommand()));
1365
+ program.addCommand(guarded(createMemoryCommand()));
1366
+ program.addCommand(guarded(createTemplateCommand()));
1367
+ var configCmd = new Command7("config").description("Manage configuration");
1368
+ configCmd.command("set <key> <value>").description("Set a config value (e.g., memory.autoInject true)").action(async (key, value) => {
1369
+ await setConfigValue(key, value);
1370
+ console.log(chalk7.green(`${key} = ${value}`));
1371
+ });
1372
+ configCmd.command("get <key>").description("Get a config value").action(async (key) => {
1373
+ const value = await getConfigValue(key);
1374
+ if (value === void 0) {
1375
+ console.error(chalk7.red(`Config key "${key}" not found.`));
1376
+ process.exit(1);
1377
+ }
1378
+ console.log(JSON.stringify(value, null, 2));
1379
+ });
1380
+ configCmd.action(async () => {
1381
+ const config = await loadConfig();
1382
+ console.log(JSON.stringify(config, null, 2));
1383
+ });
1384
+ program.addCommand(guarded(configCmd));
1385
+ program.argument("[prompt...]", "Send a quick prompt (shorthand for 'dproxy ask')").option("-m, --model <model>", "Model to use").option("--max-turns <n>", "Max agent turns", parseInt).option("--max-budget-usd <n>", "Max budget in USD", parseFloat).option("-o, --output-format <format>", "Output format").option("--no-memory", "Skip memory injection").option("--memory <keys>", "Inject specific memory keys").option("--no-life", "Skip life/PARA context injection").option("--no-history", "Don't save to history").option("--raw", "Print raw JSON response").option("--token-footer", "Append token usage footer to response text").option("--max-session-tokens <n>", "Reset session if context exceeds this token count", parseInt).option("-c, --continue", "Continue last conversation").option("-r, --resume <id>", "Resume a specific session").action(async (promptParts, opts) => {
1386
+ if (promptParts.length === 0) {
1387
+ program.help();
1388
+ return;
1389
+ }
1390
+ await requireInit();
1391
+ try {
1392
+ await runAsk(promptParts, opts);
1393
+ } catch (err) {
1394
+ console.error(chalk7.red(err.message));
1395
+ process.exit(1);
1396
+ }
1397
+ });
1398
+ program.parse();