codiedev 0.9.83 → 0.9.85

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.
@@ -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
+ }
@@ -21,6 +21,17 @@ export declare function apiRequest<T = unknown>(method: "GET" | "POST", path: st
21
21
  body?: Record<string, unknown>;
22
22
  query?: Record<string, string | number | undefined>;
23
23
  }): Promise<T>;
24
+ /**
25
+ * Binary twin of apiRequest: GET returns the raw body; POST sends `body` as
26
+ * application/octet-stream and returns parsed JSON. Same auth and errors
27
+ * (`status` on non-2xx). Used for starter-pack files (SIG-127).
28
+ */
29
+ export declare function apiRequestBytes(method: "GET" | "POST", path: string, options: {
30
+ config: CodiedevConfig;
31
+ body?: Buffer;
32
+ query?: Record<string, string | number | undefined>;
33
+ maxBytes?: number;
34
+ }): Promise<Buffer>;
24
35
  /**
25
36
  * Fire-and-forget per-invocation telemetry from CLI subcommands. Mirrors
26
37
  * the MCP server's emitTelemetry but stamps `source: "cli"` so adoption
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.applyConfigEnvOverrides = applyConfigEnvOverrides;
37
37
  exports.requireConfig = requireConfig;
38
38
  exports.apiRequest = apiRequest;
39
+ exports.apiRequestBytes = apiRequestBytes;
39
40
  exports.recordAgentEvent = recordAgentEvent;
40
41
  exports.recordInstallEvent = recordInstallEvent;
41
42
  exports.withTelemetry = withTelemetry;
@@ -135,6 +136,62 @@ function apiRequest(method, path, options) {
135
136
  req.end();
136
137
  });
137
138
  }
139
+ /**
140
+ * Binary twin of apiRequest: GET returns the raw body; POST sends `body` as
141
+ * application/octet-stream and returns parsed JSON. Same auth and errors
142
+ * (`status` on non-2xx). Used for starter-pack files (SIG-127).
143
+ */
144
+ function apiRequestBytes(method, path, options) {
145
+ return new Promise((resolve, reject) => {
146
+ const url = new URL(path, options.config.backendUrl);
147
+ for (const [k, v] of Object.entries(options.query ?? {})) {
148
+ if (v !== undefined)
149
+ url.searchParams.set(k, String(v));
150
+ }
151
+ const lib = url.protocol === "https:" ? https : http;
152
+ const req = lib.request({
153
+ hostname: url.hostname,
154
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
155
+ path: url.pathname + url.search,
156
+ method,
157
+ headers: {
158
+ Authorization: `Bearer ${options.config.token}`,
159
+ "X-Codiedev-Cli-Version": version_1.CLI_VERSION,
160
+ ...(options.body ? { "Content-Type": "application/octet-stream", "Content-Length": options.body.length } : {}),
161
+ },
162
+ }, (res) => {
163
+ const chunks = [];
164
+ let size = 0;
165
+ res.on("data", (c) => {
166
+ size += c.length;
167
+ if (options.maxBytes !== undefined && size > options.maxBytes) {
168
+ req.destroy(Object.assign(new Error(`response over ${options.maxBytes} bytes`), { status: res.statusCode }));
169
+ return;
170
+ }
171
+ chunks.push(c);
172
+ });
173
+ res.on("end", () => {
174
+ const buf = Buffer.concat(chunks);
175
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
176
+ return resolve(buf);
177
+ let message = `HTTP ${res.statusCode}`;
178
+ try {
179
+ const parsed = JSON.parse(buf.toString("utf8"));
180
+ if (parsed && typeof parsed.error === "string")
181
+ message = parsed.error;
182
+ }
183
+ catch {
184
+ // not JSON
185
+ }
186
+ reject(Object.assign(new Error(message), { status: res.statusCode }));
187
+ });
188
+ });
189
+ req.on("error", reject);
190
+ if (options.body)
191
+ req.write(options.body);
192
+ req.end();
193
+ });
194
+ }
138
195
  /**
139
196
  * Fire-and-forget per-invocation telemetry from CLI subcommands. Mirrors
140
197
  * the MCP server's emitTelemetry but stamps `source: "cli"` so adoption
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
  }
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);
@@ -0,0 +1,46 @@
1
+ export declare const PACK_MAX_FILES = 500;
2
+ export declare const PACK_FILE_MAX_BYTES: number;
3
+ export declare const PACK_FILES_MAX_TOTAL_BYTES: number;
4
+ export declare const SHA256_HEX: RegExp;
5
+ export interface PackBundleFile {
6
+ path: string;
7
+ sha256: string;
8
+ size: number;
9
+ executable?: boolean;
10
+ }
11
+ /** Why a plugin-relative path can't be written, or null when it's fine. */
12
+ export declare function packFilePathProblem(raw: unknown): string | null;
13
+ export declare function blobsDir(root: string): string;
14
+ export declare function blobPath(dir: string, sha256: string): string;
15
+ export declare function sha256Of(buf: Buffer): string;
16
+ /** Download every blob the manifest needs that isn't cached yet. Each body
17
+ * must match its sha256 and size or nothing is kept. */
18
+ export declare function ensureBlobs(dir: string, files: PackBundleFile[], download: (file: PackBundleFile) => Promise<Buffer>): Promise<{
19
+ downloaded: number;
20
+ }>;
21
+ export interface FilesEntry {
22
+ dest: string;
23
+ source: string;
24
+ line: number;
25
+ }
26
+ /** The `## Files` section of a pack.md, or null when there is none (then
27
+ * `pack apply` leaves the pack's files alone). Fenced blocks are skipped so
28
+ * agent bodies can't open a section. */
29
+ export declare function parseFilesSection(markdown: string): FilesEntry[] | null;
30
+ /** pack.md with its `## Files` section (heading and body) removed. */
31
+ export declare function stripFilesSection(markdown: string): string;
32
+ export interface LocalFile extends PackBundleFile {
33
+ abs: string;
34
+ }
35
+ export declare function expandHome(p: string, home?: string): string;
36
+ /** Walk each `## Files` entry. Junk (caches, VCS, editor files) is skipped
37
+ * quietly; symlinks, hidden files and a skill folder's own SKILL.md are
38
+ * skipped with a note. Throws on a missing source or a bad destination. */
39
+ export declare function collectLocalFiles(entries: FilesEntry[], baseDir: string, home?: string): {
40
+ files: LocalFile[];
41
+ notes: string[];
42
+ };
43
+ /** Only Claude Code sets ${CLAUDE_PLUGIN_ROOT}. In the copy other tools read,
44
+ * replace it with the plugin dir's real path (forward slashes, which every
45
+ * shell and Python accept, Windows included). */
46
+ export declare function rewritePluginRoot(content: string, pluginRoot: string): string;