codiedev 0.9.82 → 0.9.84

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/cli.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * codiedev ping <user> "<msg>" — send a message (optionally --with <key>)
10
10
  * codiedev inbox — list pings (use --unread to filter)
11
11
  * codiedev read <ping-id> — mark read + show full message
12
+ * codiedev reply <ping-id> "<msg>" — answer a ping in its thread
12
13
  * codiedev note "<text>" — capture a thought
13
14
  * codiedev help — show this help
14
15
  */
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * codiedev ping <user> "<msg>" — send a message (optionally --with <key>)
11
11
  * codiedev inbox — list pings (use --unread to filter)
12
12
  * codiedev read <ping-id> — mark read + show full message
13
+ * codiedev reply <ping-id> "<msg>" — answer a ping in its thread
13
14
  * codiedev note "<text>" — capture a thought
14
15
  * codiedev help — show this help
15
16
  */
@@ -53,8 +54,8 @@ const utils_1 = require("./utils");
53
54
  const push_1 = require("./commands/push");
54
55
  const delete_1 = require("./commands/delete");
55
56
  const pull_1 = require("./commands/pull");
56
- const ask_1 = require("./commands/ask");
57
57
  const ping_1 = require("./commands/ping");
58
+ const reply_1 = require("./commands/reply");
58
59
  const inbox_1 = require("./commands/inbox");
59
60
  const note_1 = require("./commands/note");
60
61
  const doctor_1 = require("./commands/doctor");
@@ -100,19 +101,17 @@ Artifacts:
100
101
  codiedev delete <key> [-y] Delete every version of an artifact you
101
102
  authored (clean up bad/test pushes).
102
103
  Asks for confirmation; -y to skip.
103
- codiedev ask "<question>" Ask a natural-language question of the
104
- team's collective memory. Returns an
105
- LLM-synthesized answer with citations
106
- pointing at sessions + artifacts.
107
104
 
108
105
  Messaging:
109
- codiedev ping <user> "<msg>" [--with <key>]
106
+ codiedev ping <user> "<msg>" [--with <key>] [--file <path>]
110
107
  Send a message to a teammate
111
108
  codiedev send <key> <user> ["<msg>"] Share an artifact + ping in one step
112
109
  codiedev share <key> <user> [--role read|edit]
113
110
  Silently grant access (no notification)
114
111
  codiedev inbox [--unread] [--limit N] Show messages
115
112
  codiedev read <ping-id> Mark read + show full message
113
+ codiedev reply <ping-id> "<msg>" [--file <path>]
114
+ Answer a message in its thread
116
115
 
117
116
  Starter packs:
118
117
  codiedev pack list Packs assigned to your team
@@ -284,16 +283,36 @@ codiedev pull — fetch an artifact
284
283
  codiedev ping — send a teammate a message, optionally attached to an artifact
285
284
 
286
285
  Usage:
287
- codiedev ping <user> "<message>" [--with <artifact-key>]
286
+ codiedev ping <user> "<message>" [--with <artifact-key>] [--file <path>]
288
287
 
289
288
  <user> resolves to a teammate in your org by first name, full name,
290
289
  or email. Ambiguous matches return a list — retry with the full email.
291
290
 
291
+ --file attaches a file (repeat for up to 10, 20 MB each). They see it
292
+ in their inbox and can open it in the portal.
293
+
292
294
  Teammates are notified by email and can reply from their own agent.
295
+ To answer a message you received, use 'codiedev reply' instead: it
296
+ keeps the exchange in one thread.
293
297
 
294
298
  Examples:
295
299
  codiedev ping <teammate> "thoughts on batch vs parallel?" --with spec-cart-clear.md
296
300
  codiedev ping <teammate>@example.com "can you take this one?"
301
+ codiedev ping <teammate> "shard timings from last night" --file ./timings.csv
302
+ `.trim(),
303
+ reply: `
304
+ codiedev reply — answer a message in its thread
305
+
306
+ Usage:
307
+ codiedev reply <ping-id> "<message>" [--file <path>]
308
+
309
+ Grab the ping-id from 'codiedev inbox'. The reply goes to whoever sent
310
+ that ping, stays in the same conversation, and marks their message
311
+ read and answered, so it stops showing as waiting on you.
312
+
313
+ Examples:
314
+ codiedev reply <ping-id> "yes, step 2 covers partial refunds"
315
+ codiedev reply <ping-id> "updated numbers attached" --file ./timings.csv
297
316
  `.trim(),
298
317
  inbox: `
299
318
  codiedev inbox — show messages from teammates
@@ -415,13 +434,12 @@ signal. The stored artifact type still follows the filename rule
415
434
 
416
435
  ## Portal
417
436
 
418
- Everything you do in the CLI is visible at:
437
+ Messages you send and receive are also at:
419
438
 
420
439
  https://codiedev.com/portal/inbox
421
440
 
422
- Your CTO (or any non-technical teammate) can follow along without ever
423
- touching a terminal — they read the artifacts in the portal and get
424
- email notifications when pinged.
441
+ A teammate can read and answer there without touching a terminal, and
442
+ gets an email when pinged.
425
443
 
426
444
  ## Agent-native
427
445
 
@@ -564,8 +582,10 @@ async function main() {
564
582
  await (0, pull_1.runPull)(rest);
565
583
  return;
566
584
  case "ask":
567
- await (0, ask_1.runAsk)(rest);
568
- return;
585
+ // Removed 2026-11. Kept as a named case so the answer is a pointer,
586
+ // not "unknown command".
587
+ console.error('codiedev ask has been removed. Use: codiedev search "<query>"');
588
+ process.exit(1);
569
589
  case "ping":
570
590
  await (0, ping_1.runPing)(rest);
571
591
  return;
@@ -582,6 +602,9 @@ async function main() {
582
602
  case "read":
583
603
  await (0, inbox_1.runRead)(rest);
584
604
  return;
605
+ case "reply":
606
+ await (0, reply_1.runReply)(rest);
607
+ return;
585
608
  case "scrub": {
586
609
  const { runScrubCommand } = await Promise.resolve().then(() => __importStar(require("./commands/scrub")));
587
610
  process.exitCode = await runScrubCommand(rest);
@@ -107,7 +107,16 @@ async function runRead(args) {
107
107
  console.log();
108
108
  console.log(ping.body || "(empty)");
109
109
  console.log();
110
+ if (ping.attachments?.length) {
111
+ console.log(ping.attachments.length === 1 ? "File:" : "Files:");
112
+ for (const a of ping.attachments) {
113
+ console.log(` ${(0, inboxFormat_1.fileList)([a])}`);
114
+ console.log(` ${a.url ?? "(no longer stored)"}`);
115
+ }
116
+ console.log();
117
+ }
110
118
  console.log("✓ Marked as read");
119
+ console.log(`Reply: codiedev reply ${ping.pingId} "<message>"`);
111
120
  if (ping.subjectKey) {
112
121
  console.log(`Pull: codiedev pull ${ping.subjectKey}`);
113
122
  }
@@ -2,7 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runPing = runPing;
4
4
  const shared_1 = require("./shared");
5
- function parseArgs(args) {
5
+ const pingFiles_1 = require("./pingFiles");
6
+ function parseArgs(allArgs) {
7
+ const { files, rest: args } = (0, pingFiles_1.takeFileArgs)(allArgs);
6
8
  let to;
7
9
  const positional = [];
8
10
  let subjectKey;
@@ -18,31 +20,33 @@ function parseArgs(args) {
18
20
  positional.push(a);
19
21
  }
20
22
  }
21
- if (positional.length < 2) {
22
- console.error("Usage: codiedev ping <user> \"<message>\" [--with <artifact-key>]");
23
+ // A file can stand in for the message.
24
+ if (positional.length < 2 && !(positional.length === 1 && files.length > 0)) {
25
+ console.error("Usage: codiedev ping <user> \"<message>\" [--with <artifact-key>] [--file <path>]");
23
26
  process.exit(1);
24
27
  }
25
28
  to = positional[0];
26
29
  // Join remaining positionals in case the shell split the message on spaces
27
30
  // despite quoting (happens with some escape sequences).
28
31
  const body = (0, shared_1.stripQuotes)(positional.slice(1).join(" "));
29
- return { to, body, subjectKey };
32
+ return { to, body: body || (files.length ? "Sent a file." : ""), subjectKey, files };
30
33
  }
31
34
  async function runPing(args) {
32
- const { to, body, subjectKey } = parseArgs(args);
35
+ const { to, body, subjectKey, files } = parseArgs(args);
33
36
  const config = (0, shared_1.requireConfig)();
34
37
  const start = Date.now();
35
38
  try {
39
+ const attachmentIds = await (0, pingFiles_1.uploadOrExit)(config, files);
36
40
  const res = await (0, shared_1.apiRequest)("POST", "/api/cli/ping", {
37
41
  config,
38
- body: { to, body, subjectKey },
42
+ body: { to, body, subjectKey, ...(attachmentIds.length ? { attachmentIds } : {}) },
39
43
  });
40
44
  (0, shared_1.recordAgentEvent)(config, {
41
45
  tool: "codiedev_ping",
42
46
  ok: true,
43
47
  latencyMs: Date.now() - start,
44
48
  });
45
- console.log(`✓ Pinged ${res.recipient.name} <${res.recipient.email}>${subjectKey ? ` on ${subjectKey}` : ""}`);
49
+ console.log(`✓ Pinged ${res.recipient.name} <${res.recipient.email}>${subjectKey ? ` on ${subjectKey}` : ""}${files.length ? ` with ${files.length} file${files.length === 1 ? "" : "s"}` : ""}`);
46
50
  }
47
51
  catch (err) {
48
52
  const e = err;
@@ -0,0 +1,22 @@
1
+ import type { CodiedevConfig } from "../utils";
2
+ export declare const MAX_PING_FILES = 10;
3
+ export declare const MAX_PING_FILE_BYTES: number;
4
+ export declare function contentTypeFor(fileName: string): string;
5
+ export declare function formatBytes(bytes: number): string;
6
+ /** Pull every `--file <path>` / `-f <path>` / `--file=<path>` out of argv,
7
+ * returning the paths and the remaining args untouched. */
8
+ export declare function takeFileArgs(args: string[]): {
9
+ files: string[];
10
+ rest: string[];
11
+ };
12
+ export type FileProblem = {
13
+ path: string;
14
+ reason: string;
15
+ };
16
+ /** Check every path before uploading any, so one bad path can't leave the
17
+ * others uploaded and unsent. */
18
+ export declare function checkFiles(paths: string[]): FileProblem[];
19
+ /** Upload in order; the returned ids go in the ping's `attachmentIds`. */
20
+ export declare function uploadPingFiles(config: CodiedevConfig, paths: string[]): Promise<string[]>;
21
+ /** Validate, then upload. Prints the problems and exits on a bad path. */
22
+ export declare function uploadOrExit(config: CodiedevConfig, paths: string[]): Promise<string[]>;
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAX_PING_FILE_BYTES = exports.MAX_PING_FILES = void 0;
7
+ exports.contentTypeFor = contentTypeFor;
8
+ exports.formatBytes = formatBytes;
9
+ exports.takeFileArgs = takeFileArgs;
10
+ exports.checkFiles = checkFiles;
11
+ exports.uploadPingFiles = uploadPingFiles;
12
+ exports.uploadOrExit = uploadOrExit;
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const http_1 = __importDefault(require("http"));
15
+ const https_1 = __importDefault(require("https"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const version_1 = require("../version");
18
+ // Files sent with `codiedev ping --file` / `codiedev reply --file`. The server
19
+ // stores each one and hands back an id; the ping then names those ids. Limits
20
+ // mirror the server's so a too-large file fails here, before the upload.
21
+ exports.MAX_PING_FILES = 10;
22
+ exports.MAX_PING_FILE_BYTES = 20 * 1024 * 1024;
23
+ const CONTENT_TYPES = {
24
+ csv: "text/csv",
25
+ tsv: "text/tab-separated-values",
26
+ txt: "text/plain",
27
+ log: "text/plain",
28
+ md: "text/markdown",
29
+ json: "application/json",
30
+ yaml: "text/plain",
31
+ yml: "text/plain",
32
+ diff: "text/plain",
33
+ patch: "text/plain",
34
+ png: "image/png",
35
+ jpg: "image/jpeg",
36
+ jpeg: "image/jpeg",
37
+ gif: "image/gif",
38
+ webp: "image/webp",
39
+ pdf: "application/pdf",
40
+ zip: "application/zip",
41
+ };
42
+ function contentTypeFor(fileName) {
43
+ const ext = path_1.default.extname(fileName).slice(1).toLowerCase();
44
+ return CONTENT_TYPES[ext] ?? "application/octet-stream";
45
+ }
46
+ function formatBytes(bytes) {
47
+ if (bytes < 1024)
48
+ return `${bytes} B`;
49
+ if (bytes < 1024 * 1024)
50
+ return `${Math.round(bytes / 1024)} KB`;
51
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
52
+ }
53
+ /** Pull every `--file <path>` / `-f <path>` / `--file=<path>` out of argv,
54
+ * returning the paths and the remaining args untouched. */
55
+ function takeFileArgs(args) {
56
+ const files = [];
57
+ const rest = [];
58
+ for (let i = 0; i < args.length; i++) {
59
+ const a = args[i];
60
+ if ((a === "--file" || a === "-f") && i + 1 < args.length)
61
+ files.push(args[++i]);
62
+ else if (a.startsWith("--file="))
63
+ files.push(a.slice("--file=".length));
64
+ else
65
+ rest.push(a);
66
+ }
67
+ return { files, rest };
68
+ }
69
+ /** Check every path before uploading any, so one bad path can't leave the
70
+ * others uploaded and unsent. */
71
+ function checkFiles(paths) {
72
+ const problems = [];
73
+ if (paths.length > exports.MAX_PING_FILES) {
74
+ problems.push({ path: "--file", reason: `at most ${exports.MAX_PING_FILES} files per message` });
75
+ }
76
+ for (const p of paths) {
77
+ let stat;
78
+ try {
79
+ stat = fs_1.default.statSync(p);
80
+ }
81
+ catch {
82
+ problems.push({ path: p, reason: "file not found" });
83
+ continue;
84
+ }
85
+ if (!stat.isFile())
86
+ problems.push({ path: p, reason: "not a file" });
87
+ else if (stat.size === 0)
88
+ problems.push({ path: p, reason: "file is empty" });
89
+ else if (stat.size > exports.MAX_PING_FILE_BYTES) {
90
+ problems.push({ path: p, reason: `larger than 20 MB (${formatBytes(stat.size)})` });
91
+ }
92
+ }
93
+ return problems;
94
+ }
95
+ function uploadOne(config, filePath) {
96
+ return new Promise((resolve, reject) => {
97
+ const name = path_1.default.basename(filePath);
98
+ const data = fs_1.default.readFileSync(filePath);
99
+ const url = new URL("/api/cli/ping-attachment", config.backendUrl);
100
+ url.searchParams.set("name", name);
101
+ const lib = url.protocol === "https:" ? https_1.default : http_1.default;
102
+ const req = lib.request({
103
+ hostname: url.hostname,
104
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
105
+ path: url.pathname + url.search,
106
+ method: "POST",
107
+ headers: {
108
+ Authorization: `Bearer ${config.token}`,
109
+ "X-Codiedev-Cli-Version": version_1.CLI_VERSION,
110
+ "Content-Type": contentTypeFor(name),
111
+ "Content-Length": data.length,
112
+ },
113
+ }, (res) => {
114
+ let chunks = "";
115
+ res.on("data", (c) => (chunks += c));
116
+ res.on("end", () => {
117
+ let parsed = {};
118
+ try {
119
+ parsed = chunks ? JSON.parse(chunks) : {};
120
+ }
121
+ catch {
122
+ /* fall through to the status check */
123
+ }
124
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300 && parsed.attachment?.id) {
125
+ resolve(parsed.attachment.id);
126
+ }
127
+ else {
128
+ reject(new Error(`${name}: ${parsed.error ?? `HTTP ${res.statusCode}`}`));
129
+ }
130
+ });
131
+ });
132
+ req.on("error", (err) => reject(new Error(`${name}: ${err.message}`)));
133
+ req.write(data);
134
+ req.end();
135
+ });
136
+ }
137
+ /** Upload in order; the returned ids go in the ping's `attachmentIds`. */
138
+ async function uploadPingFiles(config, paths) {
139
+ const ids = [];
140
+ for (const p of paths)
141
+ ids.push(await uploadOne(config, p));
142
+ return ids;
143
+ }
144
+ /** Validate, then upload. Prints the problems and exits on a bad path. */
145
+ async function uploadOrExit(config, paths) {
146
+ if (paths.length === 0)
147
+ return [];
148
+ const problems = checkFiles(paths);
149
+ if (problems.length) {
150
+ for (const p of problems)
151
+ console.error(`Can't attach ${p.path}: ${p.reason}`);
152
+ process.exit(1);
153
+ }
154
+ return await uploadPingFiles(config, paths);
155
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Answer a ping. Unlike a fresh `codiedev ping` back, this threads under the
3
+ * original and marks it read + replied, so it stops counting as waiting.
4
+ */
5
+ export declare function runReply(args: string[]): Promise<void>;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runReply = runReply;
4
+ const shared_1 = require("./shared");
5
+ const pingFiles_1 = require("./pingFiles");
6
+ const USAGE = 'Usage: codiedev reply <ping-id> "<message>" [--file <path>]';
7
+ /**
8
+ * Answer a ping. Unlike a fresh `codiedev ping` back, this threads under the
9
+ * original and marks it read + replied, so it stops counting as waiting.
10
+ */
11
+ async function runReply(args) {
12
+ const { files, rest } = (0, pingFiles_1.takeFileArgs)(args);
13
+ const positional = rest.filter((a) => !a.startsWith("--"));
14
+ const pingId = positional[0];
15
+ // Join the rest in case the shell split the message despite quoting.
16
+ const body = (0, shared_1.stripQuotes)(positional.slice(1).join(" "));
17
+ if (!pingId || (!body && files.length === 0)) {
18
+ console.error(USAGE);
19
+ process.exit(1);
20
+ }
21
+ const config = (0, shared_1.requireConfig)();
22
+ const start = Date.now();
23
+ try {
24
+ const attachmentIds = await (0, pingFiles_1.uploadOrExit)(config, files);
25
+ const res = await (0, shared_1.apiRequest)("POST", "/api/cli/reply", {
26
+ config,
27
+ body: { pingId, body, ...(attachmentIds.length ? { attachmentIds } : {}) },
28
+ });
29
+ (0, shared_1.recordAgentEvent)(config, {
30
+ tool: "codiedev_reply",
31
+ ok: true,
32
+ latencyMs: Date.now() - start,
33
+ });
34
+ const to = res.recipient ? `${res.recipient.name} <${res.recipient.email}>` : "them";
35
+ const withFiles = files.length ? ` with ${files.length} file${files.length === 1 ? "" : "s"}` : "";
36
+ console.log(`✓ Replied to ${to}${withFiles}`);
37
+ }
38
+ catch (err) {
39
+ const e = err;
40
+ (0, shared_1.recordAgentEvent)(config, {
41
+ tool: "codiedev_reply",
42
+ ok: false,
43
+ latencyMs: Date.now() - start,
44
+ error: e.message,
45
+ });
46
+ if (e.status === 404 && /ping not found/.test(e.message)) {
47
+ console.error(`No ping ${pingId} addressed to you. Run 'codiedev inbox' for the ids.`);
48
+ }
49
+ else {
50
+ console.error(`Reply failed: ${e.message}`);
51
+ }
52
+ process.exit(1);
53
+ }
54
+ }
package/dist/hook.js CHANGED
@@ -54,6 +54,7 @@ const selfUpdate_1 = require("./selfUpdate");
54
54
  const version_1 = require("./version");
55
55
  // --- starter packs (SIG-113 phase 2): autosync, nudge ---
56
56
  const sync_1 = require("./packs/sync");
57
+ const unreadNotice_1 = require("./unreadNotice");
57
58
  // --- end starter packs ---
58
59
  const captureLog_1 = require("./captureLog");
59
60
  const subagents_1 = require("./subagents");
@@ -267,25 +268,28 @@ async function postSessionSkip(config, sessionId, reason) {
267
268
  }
268
269
  // SessionStart: the capture-coverage denominator. Record that a session opened
269
270
  // so the backend can later compute captured ÷ started. Deliberately tiny — no
270
- // transcript read, no git — so it never slows session startup. Best-effort +
271
- // silent (SessionStart hook output is ignored by Claude Code).
271
+ // transcript read, no git — so it never slows session startup. Best-effort.
272
+ // The response may carry the person's unread-ping summary, which the caller
273
+ // turns into a one-line notice; it rides on this request so the reminder
274
+ // costs no extra round trip.
272
275
  async function runSessionStart(hookInput, source = "claude-code") {
273
276
  const config = (0, utils_1.readConfig)();
274
277
  if (!config)
275
- return;
278
+ return undefined;
276
279
  if (hookInput.session_id) {
277
280
  try {
278
- await postJson(`${config.backendUrl}/api/cli/sessionStart`, {
281
+ const res = await postJsonForResponse(`${config.backendUrl}/api/cli/sessionStart`, {
279
282
  sessionId: hookInput.session_id,
280
283
  startedAt: Date.now(),
281
284
  source,
282
285
  }, config.token, PING_TIMEOUT_MS);
286
+ return res?.unreadPings;
283
287
  }
284
288
  catch {
285
289
  /* best-effort — never block session startup */
286
290
  }
287
291
  }
288
- return;
292
+ return undefined;
289
293
  }
290
294
  function postJson(url, body, bearerToken,
291
295
  // Socket timeout. Pass a SMALL value for tiny pings that run on (or near)
@@ -337,7 +341,10 @@ timeoutMs) {
337
341
  // postJson variant that returns the parsed response body — the upload-URL
338
342
  // handshake needs the { uploadUrl } the server hands back. No timeout: runs
339
343
  // only in detached uploader children, never on the shutdown clock.
340
- function postJsonForResponse(url, body, bearerToken) {
344
+ function postJsonForResponse(url, body, bearerToken,
345
+ // Socket timeout for tiny pings on the session-start path: a black-holed
346
+ // connection must not hang the hook. Unset = no timeout, as before.
347
+ timeoutMs) {
341
348
  return new Promise((resolve, reject) => {
342
349
  const parsed = new URL(url);
343
350
  const data = JSON.stringify(body);
@@ -371,6 +378,11 @@ function postJsonForResponse(url, body, bearerToken) {
371
378
  });
372
379
  });
373
380
  req.on("error", reject);
381
+ if (timeoutMs) {
382
+ req.setTimeout(timeoutMs, () => {
383
+ req.destroy(new Error(`timeout after ${timeoutMs}ms`));
384
+ });
385
+ }
374
386
  req.write(data);
375
387
  req.end();
376
388
  });
@@ -658,7 +670,7 @@ async function main() {
658
670
  if (packNag && process.argv[2] === "session-start")
659
671
  process.stdout.write(`${packNag}\n`);
660
672
  // --- end starter packs ---
661
- await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
673
+ const unreadPings = await runSessionStart(hookInput, process.argv[2] === "session-start-opencode"
662
674
  ? "opencode"
663
675
  : process.argv[2] === "session-start-cursor"
664
676
  ? "cursor"
@@ -667,13 +679,21 @@ async function main() {
667
679
  : process.argv[2] === "session-start-pi"
668
680
  ? "pi"
669
681
  : "claude-code");
682
+ // "A teammate is waiting on you." Claude Code reads SessionStart stdout
683
+ // into the session; Cursor and Copilot get it in their context below.
684
+ // OpenCode and pi ignore this hook's output, and Codex has no SessionStart.
685
+ const unreadNotice = process.argv[2] === "session-start-opencode" || process.argv[2] === "session-start-pi"
686
+ ? null
687
+ : (0, unreadNotice_1.takeUnreadNotice)(unreadPings);
688
+ if (unreadNotice && process.argv[2] === "session-start")
689
+ process.stdout.write(`${unreadNotice}\n`);
670
690
  if ((0, utils_1.readConfig)() && (process.argv[2] === "session-start-cursor" || process.argv[2] === "session-start-copilot")) {
671
691
  const source = process.argv[2] === "session-start-cursor" ? "cursor" : "copilot";
672
692
  const { TEAM_SKILL_CONTEXT, teamSkillSessionOutput } = await Promise.resolve().then(() => __importStar(require("./teamSkillContext")));
673
693
  const prompt = hookInput.initialPrompt
674
694
  ?? hookInput.initial_prompt;
675
695
  const discovered = typeof prompt === "string" ? await discoverContext(prompt) : null;
676
- process.stdout.write(JSON.stringify(teamSkillSessionOutput(source, [TEAM_SKILL_CONTEXT, discovered, packNag].filter(Boolean).join("\n\n"))));
696
+ process.stdout.write(JSON.stringify(teamSkillSessionOutput(source, [TEAM_SKILL_CONTEXT, discovered, packNag, unreadNotice].filter(Boolean).join("\n\n"))));
677
697
  }
678
698
  return;
679
699
  }
@@ -11,7 +11,17 @@ export interface InboxPing {
11
11
  title?: string;
12
12
  } | null;
13
13
  preview: string;
14
+ attachments?: Array<{
15
+ name: string;
16
+ size: number;
17
+ url?: string | null;
18
+ }>;
14
19
  }
20
+ /** "shard-timings.csv (66 B), trace.log (1.2 MB)" */
21
+ export declare function fileList(files: Array<{
22
+ name: string;
23
+ size: number;
24
+ }>): string;
15
25
  export interface InboxMention {
16
26
  mentionId: string;
17
27
  createdAt: number;
@@ -1,7 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fileList = fileList;
3
4
  exports.formatInbox = formatInbox;
4
5
  const shared_1 = require("./commands/shared");
6
+ function fileSize(bytes) {
7
+ if (bytes < 1024)
8
+ return `${bytes} B`;
9
+ if (bytes < 1024 * 1024)
10
+ return `${Math.round(bytes / 1024)} KB`;
11
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
12
+ }
13
+ /** "shard-timings.csv (66 B), trace.log (1.2 MB)" */
14
+ function fileList(files) {
15
+ return files.map((f) => `${f.name} (${fileSize(f.size)})`).join(", ");
16
+ }
5
17
  /**
6
18
  * Render the inbox for BOTH surfaces — `codiedev inbox` and the MCP
7
19
  * `codiedev_inbox` tool an agent calls for "any messages?". Shared so the
@@ -47,6 +59,9 @@ function formatInbox(opts) {
47
59
  const where = p.subjectKey ? ` · on ${p.subjectKey}` : "";
48
60
  lines.push(`${dot} ${from}${where} · ${(0, shared_1.timeAgo)(p.createdAt)}`);
49
61
  lines.push(` ${p.preview || "(no body)"}`);
62
+ if (p.attachments?.length) {
63
+ lines.push(` ${p.attachments.length === 1 ? "file" : "files"}: ${fileList(p.attachments)}`);
64
+ }
50
65
  lines.push(` id: ${p.pingId}`);
51
66
  lines.push("");
52
67
  }
@@ -63,7 +78,9 @@ function formatInbox(opts) {
63
78
  lines.push(` ${m.url}`);
64
79
  lines.push("");
65
80
  }
66
- if (keptPings.length > 0)
81
+ if (keptPings.length > 0) {
67
82
  lines.push("Mark read: codiedev read <ping-id>");
83
+ lines.push('Reply: codiedev reply <ping-id> "<message>"');
84
+ }
68
85
  return lines;
69
86
  }
@@ -8,9 +8,14 @@ export declare function launcherPath(): string;
8
8
  /** True iff `cmd` invokes the stable launcher (from any state dir). */
9
9
  export declare function usesLauncher(cmd: string | undefined): boolean;
10
10
  export declare function readInstallRecord(): InstallRecord | null;
11
+ /** True if `p` is inside the OS temp folder (real paths, so /var vs /private/var on macOS match). */
12
+ export declare function isInTempDir(p: string, tmp?: string): boolean;
11
13
  /**
12
14
  * The hook script worth recording: absolute and present, and a global install over an npx cache
13
15
  * copy (npm prunes ~/.npm/_npx, and hooks pointed there go quietly dead). Null = don't record.
16
+ * Never a build in the temp folder: test runs stage one there, delete it, and if that landed in
17
+ * the real install.json every hook went quietly dead. CODIEDEV_ALLOW_TEMP_HOOK=1 (the CLI's own
18
+ * test setup) lets sandboxed tests use one.
14
19
  */
15
20
  export declare function stableHookScript(hookScript: string, globalCandidates?: string[]): string | null;
16
21
  /** Point the launcher at `hookScript`. Atomic, so a hook mid-launch never reads half a file. */
package/dist/launcher.js CHANGED
@@ -48,16 +48,18 @@ exports.LAUNCHER_FILE = void 0;
48
48
  exports.launcherPath = launcherPath;
49
49
  exports.usesLauncher = usesLauncher;
50
50
  exports.readInstallRecord = readInstallRecord;
51
+ exports.isInTempDir = isInTempDir;
51
52
  exports.stableHookScript = stableHookScript;
52
53
  exports.recordCurrentInstall = recordCurrentInstall;
53
54
  exports.ensureLauncher = ensureLauncher;
54
55
  exports.resetLauncherCacheForTests = resetLauncherCacheForTests;
55
56
  const fs = __importStar(require("fs"));
57
+ const os = __importStar(require("os"));
56
58
  const path = __importStar(require("path"));
57
59
  const configDir_1 = require("./configDir");
58
60
  exports.LAUNCHER_FILE = "codiedev-hook-launcher.cjs";
59
61
  // Bump when LAUNCHER_SOURCE changes so existing launchers get rewritten.
60
- const LAUNCHER_REVISION = 1;
62
+ const LAUNCHER_REVISION = 2;
61
63
  function launcherPath() {
62
64
  return path.join((0, configDir_1.codiedevStateDir)(), "bin", exports.LAUNCHER_FILE);
63
65
  }
@@ -85,6 +87,12 @@ try {
85
87
  if (rec && typeof rec.hookScript === "string" && fs.existsSync(rec.hookScript)) target = rec.hookScript;
86
88
  } catch {}
87
89
  if (!target && fs.existsSync(FALLBACK)) target = FALLBACK;
90
+ if (!target) {
91
+ // Both stranded (e.g. a deleted temp build): use a global install next to this node.
92
+ const prefix = path.dirname(path.dirname(process.execPath));
93
+ target = [path.join(prefix, "lib", "node_modules", "codiedev", "dist", "hook.js"),
94
+ path.join(prefix, "node_modules", "codiedev", "dist", "hook.js")].find((p) => fs.existsSync(p)) || null;
95
+ }
88
96
  if (!target) process.exit(0);
89
97
  process.env.CODIEDEV_VIA_LAUNCHER = "1";
90
98
  process.argv[1] = target;
@@ -115,15 +123,34 @@ function packageVersionFor(hookScript) {
115
123
  return undefined;
116
124
  }
117
125
  }
126
+ /** True if `p` is inside the OS temp folder (real paths, so /var vs /private/var on macOS match). */
127
+ function isInTempDir(p, tmp = os.tmpdir()) {
128
+ const real = (x) => {
129
+ try {
130
+ return fs.realpathSync(x);
131
+ }
132
+ catch {
133
+ return path.resolve(x);
134
+ }
135
+ };
136
+ const rel = path.relative(real(tmp), real(p));
137
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
138
+ }
118
139
  /**
119
140
  * The hook script worth recording: absolute and present, and a global install over an npx cache
120
141
  * copy (npm prunes ~/.npm/_npx, and hooks pointed there go quietly dead). Null = don't record.
142
+ * Never a build in the temp folder: test runs stage one there, delete it, and if that landed in
143
+ * the real install.json every hook went quietly dead. CODIEDEV_ALLOW_TEMP_HOOK=1 (the CLI's own
144
+ * test setup) lets sandboxed tests use one.
121
145
  */
122
146
  function stableHookScript(hookScript, globalCandidates = globalHookScripts()) {
123
147
  if (!hookScript || !path.isAbsolute(hookScript) || hookScript.includes(`${path.sep}~${path.sep}`))
124
148
  return null;
125
149
  if (!fs.existsSync(hookScript))
126
150
  return null;
151
+ if (process.env.CODIEDEV_ALLOW_TEMP_HOOK !== "1" && isInTempDir(hookScript)) {
152
+ return globalCandidates.find((p) => path.isAbsolute(p) && fs.existsSync(p) && !isInTempDir(p)) ?? null;
153
+ }
127
154
  if (hookScript.includes(`${path.sep}_npx${path.sep}`)) {
128
155
  const global = globalCandidates.find((p) => path.isAbsolute(p) && fs.existsSync(p));
129
156
  if (global)
package/dist/mcp.js CHANGED
@@ -291,9 +291,7 @@ const TOOLS = [
291
291
  "when the user asks 'has anyone solved X?', 'find me the spec on Y', " +
292
292
  "or 'look for prior work on Z'. Always search before pushing a new " +
293
293
  "artifact so the user doesn't duplicate work. Returns ranked hits " +
294
- "with title, type, filename key, and a snippet. For open-ended " +
295
- "questions where the user wants a synthesized answer (not just a list " +
296
- "of hits), prefer codiedev_ask instead.",
294
+ "with title, type, filename key, and a snippet.",
297
295
  inputSchema: {
298
296
  type: "object",
299
297
  properties: {
@@ -303,32 +301,6 @@ const TOOLS = [
303
301
  required: ["query"],
304
302
  },
305
303
  },
306
- {
307
- name: "codiedev_ask",
308
- description: "Ask a natural-language question of the team's collective memory and " +
309
- "get a synthesized answer with citations. Searches across both " +
310
- "auto-extracted artifacts (specs / decisions / bugfixes) AND raw " +
311
- "session transcripts, then has an LLM compose an answer that cites " +
312
- "the actual sources. Use when the user asks 'why did we…?', 'what " +
313
- "does the team know about X?', 'has anyone done Y here before?', " +
314
- "'who decided Z and why?', or any open-ended question about prior " +
315
- "work where they want context, not just a hit list. For exact-match " +
316
- "lookups (find me spec-x), prefer codiedev_pull. For browsing or " +
317
- "filtering by kind/tag, prefer codiedev_get_library. For surfacing " +
318
- "ranked hits without LLM synthesis, prefer codiedev_search.",
319
- inputSchema: {
320
- type: "object",
321
- properties: {
322
- question: {
323
- type: "string",
324
- description: "Natural-language question. Open-ended is fine — e.g. 'why " +
325
- "did we drop pg-bouncer?' or 'how does the team think about " +
326
- "feature flags?'.",
327
- },
328
- },
329
- required: ["question"],
330
- },
331
- },
332
304
  {
333
305
  name: "codiedev_get_library",
334
306
  description: "List artifacts in the team library — both docs (spec / decision / " +
@@ -467,8 +439,6 @@ async function dispatchTool(name, args, config) {
467
439
  return await handleShareWith(args, config);
468
440
  case "codiedev_send_to":
469
441
  return await handleSendTo(args, config);
470
- case "codiedev_ask":
471
- return await handleAsk(args, config);
472
442
  case "codiedev_search":
473
443
  return await handleSearch(args, config);
474
444
  case "codiedev_get_library":
@@ -740,23 +710,6 @@ async function handleSearch(args, config) {
740
710
  }
741
711
  return { content: [{ type: "text", text: lines.join("\n") }] };
742
712
  }
743
- async function handleAsk(args, config) {
744
- const question = asString(args.question);
745
- if (!question)
746
- throw new Error("question required");
747
- const res = await (0, shared_1.apiRequest)("POST", "/api/cli/ask", { config, body: { question } });
748
- const lines = [res.answer];
749
- if (res.citations && res.citations.length > 0) {
750
- lines.push("", "Sources:");
751
- for (const c of res.citations) {
752
- const label = c.kind === "artifact"
753
- ? `[${c.type}] ${c.title}`
754
- : `[session] ${c.title}`;
755
- lines.push(` • ${label}`);
756
- }
757
- }
758
- return { content: [{ type: "text", text: lines.join("\n") }] };
759
- }
760
713
  async function handleGetLibrary(args, config) {
761
714
  const scope = asStringOrUndefined(args.scope);
762
715
  const type = asStringOrUndefined(args.type);
@@ -1,5 +1,5 @@
1
1
  /** Session-start context uses each client's supported output contract. */
2
- export declare const TEAM_SKILL_CONTEXT = "This machine is connected to Codiedev, the team's artifact and skill library.\nBefore a substantial coding task, run codiedev search \"<brief task description>\" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.\nUse codiedev push <file.md> to share an artifact when asked, codiedev ask \"<question>\" for team knowledge, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.";
2
+ export declare const TEAM_SKILL_CONTEXT = "This machine is connected to Codiedev, the team's artifact and skill library.\nBefore a substantial coding task, run codiedev search \"<brief task description>\" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.\nUse codiedev push <file.md> to share an artifact when asked, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.";
3
3
  export declare function teamSkillSessionOutput(source: "cursor" | "copilot", context?: string): {
4
4
  additional_context: string;
5
5
  additionalContext?: undefined;
@@ -5,7 +5,7 @@ exports.teamSkillSessionOutput = teamSkillSessionOutput;
5
5
  /** Session-start context uses each client's supported output contract. */
6
6
  exports.TEAM_SKILL_CONTEXT = `This machine is connected to Codiedev, the team's artifact and skill library.
7
7
  Before a substantial coding task, run codiedev search "<brief task description>" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.
8
- Use codiedev push <file.md> to share an artifact when asked, codiedev ask "<question>" for team knowledge, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.`;
8
+ Use codiedev push <file.md> to share an artifact when asked, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.`;
9
9
  function teamSkillSessionOutput(source, context = exports.TEAM_SKILL_CONTEXT) {
10
10
  return source === "cursor" ? { additional_context: context } : { additionalContext: context };
11
11
  }
@@ -0,0 +1,22 @@
1
+ export interface UnreadPings {
2
+ count: number;
3
+ newestPingId: string | null;
4
+ from: string[];
5
+ senderCount?: number;
6
+ }
7
+ /** Repeat an unchanged notice at most this often. A new ping always shows. */
8
+ export declare const UNREAD_NOTICE_INTERVAL_MS: number;
9
+ export declare function parseUnreadPings(value: unknown): UnreadPings | null;
10
+ /** The line added to the session's context. Written for the agent: it says
11
+ * what is waiting and what to do about it. */
12
+ export declare function unreadNoticeLine(u: UnreadPings): string;
13
+ /**
14
+ * Returns the notice to print, or null. Shown when the newest unread ping is
15
+ * one we haven't announced, or the same pile is still there a day later.
16
+ * Records that it spoke; any failure is silent — never slow or break a
17
+ * session start over a reminder.
18
+ */
19
+ export declare function takeUnreadNotice(value: unknown, options?: {
20
+ dir?: string;
21
+ now?: number;
22
+ }): string | null;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UNREAD_NOTICE_INTERVAL_MS = void 0;
7
+ exports.parseUnreadPings = parseUnreadPings;
8
+ exports.unreadNoticeLine = unreadNoticeLine;
9
+ exports.takeUnreadNotice = takeUnreadNotice;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const configDir_1 = require("./configDir");
13
+ /** Repeat an unchanged notice at most this often. A new ping always shows. */
14
+ exports.UNREAD_NOTICE_INTERVAL_MS = 20 * 60 * 60 * 1000;
15
+ const STATE_FILE = "unread-notice.json";
16
+ function parseUnreadPings(value) {
17
+ if (!value || typeof value !== "object")
18
+ return null;
19
+ const v = value;
20
+ if (typeof v.count !== "number" || !Number.isFinite(v.count) || v.count <= 0)
21
+ return null;
22
+ return {
23
+ count: Math.floor(v.count),
24
+ newestPingId: typeof v.newestPingId === "string" ? v.newestPingId : null,
25
+ from: Array.isArray(v.from) ? v.from.filter((n) => typeof n === "string" && n.trim() !== "") : [],
26
+ senderCount: typeof v.senderCount === "number" ? v.senderCount : undefined,
27
+ };
28
+ }
29
+ function nameList(from, senderCount) {
30
+ if (from.length === 0)
31
+ return "";
32
+ const others = Math.max(0, senderCount - from.length);
33
+ const names = others > 0 ? [...from, `${others} other${others === 1 ? "" : "s"}`] : from;
34
+ if (names.length === 1)
35
+ return ` from ${names[0]}`;
36
+ return ` from ${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
37
+ }
38
+ /** The line added to the session's context. Written for the agent: it says
39
+ * what is waiting and what to do about it. */
40
+ function unreadNoticeLine(u) {
41
+ const who = nameList(u.from, u.senderCount ?? u.from.length);
42
+ return (`Codiedev: ${u.count} unread message${u.count === 1 ? "" : "s"}${who}. ` +
43
+ "Tell the user at the start of your first reply, and offer to show them with `codiedev inbox --unread`. " +
44
+ 'Answer one with `codiedev reply <ping-id> "<message>"`.');
45
+ }
46
+ function readState(dir) {
47
+ try {
48
+ const raw = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, STATE_FILE), "utf8"));
49
+ if (typeof raw?.at !== "number")
50
+ return null;
51
+ return { newestPingId: typeof raw.newestPingId === "string" ? raw.newestPingId : null, at: raw.at };
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ /**
58
+ * Returns the notice to print, or null. Shown when the newest unread ping is
59
+ * one we haven't announced, or the same pile is still there a day later.
60
+ * Records that it spoke; any failure is silent — never slow or break a
61
+ * session start over a reminder.
62
+ */
63
+ function takeUnreadNotice(value, options = {}) {
64
+ try {
65
+ const unread = parseUnreadPings(value);
66
+ if (!unread)
67
+ return null;
68
+ const dir = options.dir ?? (0, configDir_1.codiedevStateDir)();
69
+ const now = options.now ?? Date.now();
70
+ const last = readState(dir);
71
+ const sameNewest = !!last && last.newestPingId === unread.newestPingId;
72
+ if (sameNewest && last.at <= now && now - last.at < exports.UNREAD_NOTICE_INTERVAL_MS)
73
+ return null;
74
+ fs_1.default.mkdirSync(dir, { recursive: true });
75
+ fs_1.default.writeFileSync(path_1.default.join(dir, STATE_FILE), JSON.stringify({ newestPingId: unread.newestPingId, at: now }));
76
+ return unreadNoticeLine(unread);
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
package/dist/utils.js CHANGED
@@ -1673,14 +1673,14 @@ thought, use the \`codiedev\` CLI via Bash:**
1673
1673
  | "push this spec" / "save this as a skill" | \`codiedev push <file.md>\` |
1674
1674
  | "pull X" / "grab the latest spec-X" / "what did a teammate push?" | \`codiedev pull <key>\` |
1675
1675
  | "delete X" / "remove that test push" / "rm spec-old" | \`codiedev delete <key>\` (always confirm with the user first) |
1676
- | "ping a teammate about this" / "ask someone for their take" | \`codiedev ping <name> "<msg>" [--with <key>]\` |
1676
+ | "ping a teammate about this" / "ask someone for their take" / "send them this file" | \`codiedev ping <name> "<msg>" [--with <key>] [--file <path>]\` |
1677
1677
  | "any messages?" / "check my inbox" / "what did a teammate say?" | \`codiedev inbox\` |
1678
1678
  | "read that reply" / "mark that ping read" | \`codiedev read <ping-id>\` |
1679
+ | "reply to that" / "tell them X" (answering a message from \`codiedev inbox\`) | \`codiedev reply <ping-id> "<msg>" [--file <path>]\` (not a new ping: this threads it and marks theirs answered) |
1679
1680
  | "note that X is a follow-up" / "remember X" | \`codiedev note "<text>"\` |
1680
1681
  | "share with a teammate" / "give someone access" (no notification) | \`codiedev share <key> <name> [--role read\\|edit]\` |
1681
1682
  | "send this to a teammate" / "ask someone to look at this" | \`codiedev send <key> <name> ["<msg>"]\` |
1682
1683
  | "find something about X" / "has anyone solved Y?" | \`codiedev search "<query>" [--limit N]\` |
1683
- | "why did we…?" / "what does the team know about X?" | \`codiedev ask "<question>"\` (synthesized answer + citations) |
1684
1684
  | "show my library" / "what artifacts exist?" | \`codiedev library [--scope mine\\|shared\\|all] [--kind doc\\|skill]\` |
1685
1685
  | "run my agent's test cases" / "check the dataset on model Y" / "run v2 of the dataset" | \`codiedev traces cases <agent> [--dataset <name> [--version <n>]] [--model <id>]\` (writes cases + RUN_CASES.md + results.json; follow it, then \`codiedev traces report <results.json>\`) |
1686
1686
  | "how are my agents doing?" / "what's failing this week?" / "show the booking agent's failed runs" / "why did run X fail?" | \`codiedev traces overview\`; \`codiedev traces runs <agent> [--failed] [--flag <issue>] [--days n]\`; \`codiedev traces run <run> --agent <name>\` (add \`--json\` to read the data yourself) |
@@ -1695,9 +1695,10 @@ thought, use the \`codiedev\` CLI via Bash:**
1695
1695
  is classified into one of two kinds at upload time:
1696
1696
 
1697
1697
  - **skill** — the file has SKILL.md frontmatter (\`name:\` + \`description:\`
1698
- inside a \`---\` block). Lands under \`Artifacts → Skills tab\` and is
1698
+ inside a \`---\` block). Shows on the Skills page in the portal and is
1699
1699
  invokable as a slash command (\`/<name>\`).
1700
- - **doc** — everything else. Lands under \`Artifacts → My artifacts\`.
1700
+ - **doc** — everything else. Lives in the library: list it with
1701
+ \`codiedev library\`, fetch it with \`codiedev pull <key>\`.
1701
1702
 
1702
1703
  Detection is content-based, not filename-based. The filename prefix
1703
1704
  (\`spec-\` / \`decision-\` / \`proposal-\` / \`bugfix-\` / \`review-\` /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.82",
3
+ "version": "0.9.84",
4
4
  "description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",
@@ -1 +0,0 @@
1
- export declare function runAsk(args: string[]): Promise<void>;
@@ -1,58 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runAsk = runAsk;
4
- const shared_1 = require("./shared");
5
- function parseArgs(args) {
6
- // Everything after the command, joined back into a question string. Allows
7
- // both quoted and unquoted forms: `codiedev ask "why pg-bouncer?"` or
8
- // `codiedev ask why did we drop pg-bouncer`.
9
- const question = args
10
- .filter((a) => !a.startsWith("--"))
11
- .join(" ")
12
- .trim();
13
- if (!question) {
14
- console.error('Usage: codiedev ask "<question>"');
15
- console.error('Example: codiedev ask "why did we drop pg-bouncer?"');
16
- process.exit(1);
17
- }
18
- return { question };
19
- }
20
- async function runAsk(args) {
21
- const { question } = parseArgs(args);
22
- const config = (0, shared_1.requireConfig)();
23
- const start = Date.now();
24
- try {
25
- const res = await (0, shared_1.apiRequest)("POST", "/api/cli/ask", {
26
- config,
27
- body: { question },
28
- });
29
- (0, shared_1.recordAgentEvent)(config, {
30
- tool: "codiedev_ask",
31
- ok: true,
32
- latencyMs: Date.now() - start,
33
- });
34
- console.log("");
35
- console.log(res.answer);
36
- if (res.citations.length > 0) {
37
- console.log("");
38
- console.log("Sources:");
39
- for (const c of res.citations) {
40
- const label = c.kind === "artifact"
41
- ? `[${c.type}] ${c.title}`
42
- : `[session] ${c.title}`;
43
- console.log(` • ${label}`);
44
- }
45
- }
46
- console.log("");
47
- }
48
- catch (err) {
49
- (0, shared_1.recordAgentEvent)(config, {
50
- tool: "codiedev_ask",
51
- ok: false,
52
- latencyMs: Date.now() - start,
53
- error: err.message,
54
- });
55
- console.error(`Ask failed: ${err.message}`);
56
- process.exit(1);
57
- }
58
- }