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.
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,6 +2,7 @@ import { CodiedevConfig } from "../utils";
2
2
  import { InstallOptions, PackInstallResult, PackUninstallResult } from "../packs/install";
3
3
  import { HarnessName } from "../packs/harnesses";
4
4
  import { PackManifest } from "../packs/manifest";
5
+ import { PackBundleFile } from "../packs/files";
5
6
  export declare const DEFAULT_BACKEND_URL = "https://cli.codiedev.com";
6
7
  /** CODIEDEV_TOKEN alone is enough: no ~/.codiedev/config.json needed (CI).
7
8
  * CODIEDEV_URL defaults to production. Otherwise the connected config. */
@@ -22,6 +23,8 @@ export interface PackArgs {
22
23
  manifest?: boolean;
23
24
  }
24
25
  export declare function parsePackArgs(args: string[]): PackArgs;
26
+ /** Download a pack's files (SIG-127) into the blob cache, verified by sha256. */
27
+ export declare function downloadPackFiles(config: CodiedevConfig, slug: string, files: PackBundleFile[], dir: string): Promise<number>;
25
28
  export declare function runList(config: CodiedevConfig, json: boolean, overrides?: Partial<InstallOptions>): Promise<void>;
26
29
  export interface InstallRun {
27
30
  installed: PackInstallResult[];
@@ -45,10 +48,10 @@ export declare function commitExport(dir: string, message: string): string;
45
48
  /** Write a CI-committable copy of the pack: `<out>/plugins/<slug>/…` plus
46
49
  * `<out>/.claude-plugin/marketplace.json` (existing entries for other packs
47
50
  * in the same folder are kept). */
48
- export declare function exportPluginFolder(out: string, manifest: PackManifest): string;
51
+ export declare function exportPluginFolder(out: string, manifest: PackManifest, blobDir?: string): string;
49
52
  export declare function runExport(config: CodiedevConfig, args: PackArgs): Promise<{
50
53
  written: string;
51
54
  commit?: string;
52
55
  }>;
53
56
  export declare function runApply(config: CodiedevConfig, args: PackArgs): Promise<Record<string, unknown>>;
54
- export declare const PACK_HELP = "Usage:\n codiedev pack list Packs assigned to your team\n codiedev pack install [slug] [--harness claude,codex,copilot,cursor,codie,pi] [--dry-run] [--json] [--quiet]\n Install one pack, or every pack that applies\n codiedev pack status [--json] Installed packs and whether they're up to date\n codiedev pack export <slug> --out <dir> [--git] [--manifest]\n Write the pack as a Claude-format plugin folder plus\n .claude-plugin/marketplace.json (commit it for CI).\n --git inits the folder and commits; --manifest writes\n an editable <slug>.pack.md instead\n codiedev pack apply <pack.md> Create or update a pack from a pack.md\n\nCursor: packs are copied to ~/.cursor/plugins/local/codiedev-<slug>/. A Cursor\nteam admin can disable \"user local plugin imports\"; then Cursor ignores them.\n\nAuto-update: session starts run `codiedev pack install --quiet` in the background\nabout once an hour. Opt out with CODIEDEV_PACK_AUTOSYNC=0.\n\nHooks for specific skills only: codiedev hook-gate --skills a,b --cmd-b64 <base64url of the command>\n(the pack editor writes this; the command runs via /bin/sh, or cmd.exe on Windows)\n\nCI: set CODIEDEV_TOKEN to a pack CI key (cdvci_\u2026, from the pack's page in the portal).\nNo `codiedev connect` or config file is needed. CI mode (a cdvci_ key, or CI=true)\nnever auto-syncs, reports or prunes, installs the one pack the key can see, defaults\n--harness to claude when it's on PATH, and exits non-zero unless every requested tool\ngot the pack. Skills from a pack are /<pack-slug>:<skill> in Claude Code.\n\n GitLab CI (.gitlab-ci.yml; add CODIEDEV_TOKEN and ANTHROPIC_API_KEY as masked\n CI/CD variables):\n pack-scan:\n image: node:22\n script:\n - npm i -g codiedev @anthropic-ai/claude-code\n - codiedev pack install --harness claude\n - claude -p \"/platform-starter:healthcare-scan\" --output-format text\n\n GitHub Actions (repository secrets CODIEDEV_TOKEN and ANTHROPIC_API_KEY):\n - uses: actions/setup-node@v4\n with: { node-version: 22 }\n - run: npm i -g codiedev @anthropic-ai/claude-code\n - run: codiedev pack install --harness claude\n env:\n CODIEDEV_TOKEN: ${{ secrets.CODIEDEV_TOKEN }}\n - run: claude -p \"/platform-starter:healthcare-scan\" --output-format text\n env:\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n\n Or vendor the plugin into the repo instead of installing at build time:\n codiedev pack export <slug> --out .codiedev/packs";
57
+ export declare const PACK_HELP = "Usage:\n codiedev pack list Packs assigned to your team\n codiedev pack install [slug] [--harness claude,codex,copilot,cursor,codie,pi] [--dry-run] [--json] [--quiet]\n Install one pack, or every pack that applies\n codiedev pack status [--json] Installed packs and whether they're up to date\n codiedev pack export <slug> --out <dir> [--git] [--manifest]\n Write the pack as a Claude-format plugin folder plus\n .claude-plugin/marketplace.json (commit it for CI).\n --git inits the folder and commits; --manifest writes\n an editable <slug>.pack.md instead\n codiedev pack apply <pack.md> [--dry-run]\n Create or update a pack from a pack.md\n\nFiles (scripts, PDFs, a runner folder): list them under \"## Files\" in pack.md,\none \"- <path in pack> <- <local path>\" per line (local paths are relative to\nthe pack.md; \".\" is the pack root):\n\n ## Files\n\n - runner <- ~/.claude/runner\n - skills/healthcare-scan <- ~/.claude/skills/healthcare-scan\n\n`pack apply` uploads them (admins and team managers; 15MB per file, 50MB per\npack; caches, .git and hidden files are skipped). Without a \"## Files\"\nsection the pack's files are left alone. In skills, point at files relative to\nthe skill's folder, or use ${CLAUDE_PLUGIN_ROOT}/runner/...: Codiedev swaps in the\nreal path for Codex, Copilot, Cursor, pi and Codie.\n\nCursor: packs are copied to ~/.cursor/plugins/local/codiedev-<slug>/. A Cursor\nteam admin can disable \"user local plugin imports\"; then Cursor ignores them.\n\nAuto-update: session starts run `codiedev pack install --quiet` in the background\nabout once an hour. Opt out with CODIEDEV_PACK_AUTOSYNC=0.\n\nHooks for specific skills only: codiedev hook-gate --skills a,b --cmd-b64 <base64url of the command>\n(the pack editor writes this; the command runs via /bin/sh, or cmd.exe on Windows)\n\nCI: set CODIEDEV_TOKEN to a pack CI key (cdvci_\u2026, from the pack's page in the portal).\nNo `codiedev connect` or config file is needed. CI mode (a cdvci_ key, or CI=true)\nnever auto-syncs, reports or prunes, installs the one pack the key can see, defaults\n--harness to claude when it's on PATH, and exits non-zero unless every requested tool\ngot the pack. Skills from a pack are /<pack-slug>:<skill> in Claude Code.\n\n GitLab CI (.gitlab-ci.yml; add CODIEDEV_TOKEN and ANTHROPIC_API_KEY as masked\n CI/CD variables):\n pack-scan:\n image: node:22\n script:\n - npm i -g codiedev @anthropic-ai/claude-code\n - codiedev pack install --harness claude\n - claude -p \"/platform-starter:healthcare-scan\" --output-format text\n\n GitHub Actions (repository secrets CODIEDEV_TOKEN and ANTHROPIC_API_KEY):\n - uses: actions/setup-node@v4\n with: { node-version: 22 }\n - run: npm i -g codiedev @anthropic-ai/claude-code\n - run: codiedev pack install --harness claude\n env:\n CODIEDEV_TOKEN: ${{ secrets.CODIEDEV_TOKEN }}\n - run: claude -p \"/platform-starter:healthcare-scan\" --output-format text\n env:\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n\n Or vendor the plugin into the repo instead of installing at build time:\n codiedev pack export <slug> --out .codiedev/packs";
@@ -37,6 +37,7 @@ exports.PACK_HELP = exports.DEFAULT_BACKEND_URL = void 0;
37
37
  exports.resolvePackConfig = resolvePackConfig;
38
38
  exports.ciHarnesses = ciHarnesses;
39
39
  exports.parsePackArgs = parsePackArgs;
40
+ exports.downloadPackFiles = downloadPackFiles;
40
41
  exports.runList = runList;
41
42
  exports.runInstall = runInstall;
42
43
  exports.runStatus = runStatus;
@@ -59,6 +60,7 @@ const harnesses_2 = require("../packs/harnesses");
59
60
  const os = __importStar(require("os"));
60
61
  const utils_1 = require("../utils");
61
62
  const shared_1 = require("./shared");
63
+ const files_1 = require("../packs/files");
62
64
  // --- CI (pack-scoped `cdvci_` keys) ------------------------------------------
63
65
  exports.DEFAULT_BACKEND_URL = "https://cli.codiedev.com";
64
66
  /** CODIEDEV_TOKEN alone is enough: no ~/.codiedev/config.json needed (CI).
@@ -101,7 +103,23 @@ const USAGE = `Usage:
101
103
  .claude-plugin/marketplace.json (commit it for CI).
102
104
  --git inits the folder and commits; --manifest writes
103
105
  an editable <slug>.pack.md instead
104
- codiedev pack apply <pack.md> Create or update a pack from a pack.md
106
+ codiedev pack apply <pack.md> [--dry-run]
107
+ Create or update a pack from a pack.md
108
+
109
+ Files (scripts, PDFs, a runner folder): list them under "## Files" in pack.md,
110
+ one "- <path in pack> <- <local path>" per line (local paths are relative to
111
+ the pack.md; "." is the pack root):
112
+
113
+ ## Files
114
+
115
+ - runner <- ~/.claude/runner
116
+ - skills/healthcare-scan <- ~/.claude/skills/healthcare-scan
117
+
118
+ \`pack apply\` uploads them (admins and team managers; 15MB per file, 50MB per
119
+ pack; caches, .git and hidden files are skipped). Without a "## Files"
120
+ section the pack's files are left alone. In skills, point at files relative to
121
+ the skill's folder, or use \${CLAUDE_PLUGIN_ROOT}/runner/...: Codiedev swaps in the
122
+ real path for Codex, Copilot, Cursor, pi and Codie.
105
123
 
106
124
  Cursor: packs are copied to ~/.cursor/plugins/local/codiedev-<slug>/. A Cursor
107
125
  team admin can disable "user local plugin imports"; then Cursor ignores them.
@@ -188,6 +206,16 @@ async function fetchManifest(config, slug) {
188
206
  const raw = await (0, shared_1.apiRequest)("GET", "/api/cli/packs/manifest", { config, query: { slug } });
189
207
  return (0, manifest_1.parseManifest)(raw);
190
208
  }
209
+ /** Download a pack's files (SIG-127) into the blob cache, verified by sha256. */
210
+ async function downloadPackFiles(config, slug, files, dir) {
211
+ if (!files.length)
212
+ return 0;
213
+ const { downloaded } = await (0, files_1.ensureBlobs)(dir, files, (file) => (0, shared_1.apiRequestBytes)("GET", "/api/cli/packs/file", { config, query: { slug, sha256: file.sha256 }, maxBytes: files_1.PACK_FILE_MAX_BYTES + 1 })
214
+ .catch((error) => {
215
+ throw new Error(`couldn't download ${file.path} (${error.message})`);
216
+ }));
217
+ return downloaded;
218
+ }
191
219
  async function runList(config, json, overrides = {}) {
192
220
  const packs = await listPacks(config);
193
221
  const registry = (0, store_1.readRegistry)((0, paths_1.packsRoot)(overrides.env, overrides.home));
@@ -206,6 +234,7 @@ async function runList(config, json, overrides = {}) {
206
234
  `${p.itemCount ?? 0} skill${p.itemCount === 1 ? "" : "s"}`,
207
235
  `${p.agentCount ?? 0} agent${p.agentCount === 1 ? "" : "s"}`,
208
236
  ...(p.hasHooks ? ["hooks"] : []),
237
+ ...(p.fileCount ? [`${p.fileCount} file${p.fileCount === 1 ? "" : "s"}`] : []),
209
238
  ].join(", ");
210
239
  const channel = (0, manifest_2.cleanLabel)(p.channel, 16);
211
240
  console.log(`${p.slug} v${p.version}${channel ? ` [${channel}]` : ""} ${(0, manifest_2.displayName)(p)} (${counts}) — ${state}`);
@@ -244,8 +273,27 @@ async function runInstall(config, args, overrides = {}) {
244
273
  // A listed pack that 404s a moment later (race with an unassign) is skipped.
245
274
  }
246
275
  }
247
- let installed = manifests.length
248
- ? (0, install_1.installPacks)({ ...overrides, manifests, harnesses, dryRun: args.dryRun })
276
+ // Pack files first: a pack whose files can't be fetched keeps its previous
277
+ // install untouched rather than shipping half its folders.
278
+ const blobDir = overrides.blobDir ?? (0, files_1.blobsDir)(root);
279
+ const ready = [];
280
+ let downloadFailed = false;
281
+ for (const manifest of manifests) {
282
+ if (args.dryRun || !manifest.files?.length) {
283
+ ready.push(manifest);
284
+ continue;
285
+ }
286
+ try {
287
+ await downloadPackFiles(config, manifest.slug, manifest.files, blobDir);
288
+ ready.push(manifest);
289
+ }
290
+ catch (error) {
291
+ downloadFailed = true;
292
+ console.error(`✗ ${manifest.slug}: ${error.message}`);
293
+ }
294
+ }
295
+ let installed = ready.length
296
+ ? (0, install_1.installPacks)({ ...overrides, manifests: ready, harnesses, dryRun: args.dryRun, blobDir })
249
297
  : [];
250
298
  let removed = [];
251
299
  if (!args.dryRun && !ci) {
@@ -293,7 +341,7 @@ async function runInstall(config, args, overrides = {}) {
293
341
  const ciFailed = ci && !args.dryRun && (installed.length === 0 || installed.some((p) => p.results.some((r) => r.status !== "ok")));
294
342
  if (ci && !args.dryRun && installed.length === 0)
295
343
  console.error("No pack is visible to this key.");
296
- if (failed || ciFailed || (args.slug && removed.length + revoked.length > 0))
344
+ if (failed || ciFailed || downloadFailed || (args.slug && removed.length + revoked.length > 0))
297
345
  process.exitCode = 1;
298
346
  return run;
299
347
  }
@@ -494,9 +542,9 @@ function readMarketplaceEntries(dir) {
494
542
  /** Write a CI-committable copy of the pack: `<out>/plugins/<slug>/…` plus
495
543
  * `<out>/.claude-plugin/marketplace.json` (existing entries for other packs
496
544
  * in the same folder are kept). */
497
- function exportPluginFolder(out, manifest) {
545
+ function exportPluginFolder(out, manifest, blobDir) {
498
546
  fs.mkdirSync(out, { recursive: true });
499
- const dir = (0, store_1.writePluginDir)(out, manifest);
547
+ const dir = (0, store_1.writePluginDir)(out, manifest, { blobDir });
500
548
  const entries = readMarketplaceEntries(out).filter((e) => e.slug !== manifest.slug);
501
549
  entries.push({ slug: manifest.slug, description: manifest.description || manifest.name, version: manifest.version });
502
550
  (0, store_1.writeMarketplace)(out, entries);
@@ -524,7 +572,24 @@ async function runExport(config, args) {
524
572
  fs.mkdirSync(out, { recursive: true });
525
573
  // <slug>.pack.md so several packs can share one folder.
526
574
  written = path.join(out, `${args.slug}.pack.md`);
527
- (0, store_1.writeFileAtomic)(written, res.markdown.endsWith("\n") ? res.markdown : `${res.markdown}\n`);
575
+ let markdown = res.markdown.endsWith("\n") ? res.markdown : `${res.markdown}\n`;
576
+ // Pack files go beside it in <slug>.files/, mapped back with `## Files`
577
+ // so `pack apply` round-trips them.
578
+ const files = (0, manifest_1.parseManifest)({ slug: args.slug, version: 0, contentHash: "-", files: res.files }).files ?? [];
579
+ if (files.length) {
580
+ const blobDir = (0, files_1.blobsDir)((0, paths_1.packsRoot)());
581
+ await downloadPackFiles(config, args.slug, files, blobDir);
582
+ const filesDir = path.join(out, `${args.slug}.files`);
583
+ fs.rmSync(filesDir, { recursive: true, force: true });
584
+ for (const f of files) {
585
+ const target = path.join(filesDir, ...f.path.split("/"));
586
+ fs.mkdirSync(path.dirname(target), { recursive: true });
587
+ fs.copyFileSync((0, files_1.blobPath)(blobDir, f.sha256), target);
588
+ fs.chmodSync(target, f.executable ? 0o755 : 0o644);
589
+ }
590
+ markdown = `${markdown.trimEnd()}\n\n## Files\n\n- . <- ./${args.slug}.files\n`;
591
+ }
592
+ (0, store_1.writeFileAtomic)(written, markdown);
528
593
  message = `codiedev pack export ${args.slug} (${args.slug}.pack.md)`;
529
594
  }
530
595
  else {
@@ -539,7 +604,9 @@ async function runExport(config, args) {
539
604
  throw new Error(`Refusing to export ${args.slug}: ${error.message}`);
540
605
  throw error;
541
606
  }
542
- written = exportPluginFolder(out, manifest);
607
+ const blobDir = (0, files_1.blobsDir)((0, paths_1.packsRoot)());
608
+ await downloadPackFiles(config, manifest.slug, manifest.files ?? [], blobDir);
609
+ written = exportPluginFolder(out, manifest, blobDir);
543
610
  message = `codiedev pack export ${manifest.slug} v${manifest.version}`;
544
611
  }
545
612
  const commit = args.git ? commitExport(out, message) : undefined;
@@ -567,13 +634,31 @@ async function runApply(config, args) {
567
634
  }
568
635
  if (!markdown.trim())
569
636
  throw new Error(`${file} is empty`);
637
+ // `## Files` (SIG-127): local folders/files to ship in the plugin dir.
638
+ // Absent ⇒ the pack's files are left as they are.
639
+ const entries = (0, files_1.parseFilesSection)(markdown);
640
+ const local = entries ? (0, files_1.collectLocalFiles)(entries, path.dirname(file)) : null;
641
+ for (const note of local?.notes ?? [])
642
+ console.log(` · ${note}`);
570
643
  if (args.dryRun) {
571
- console.log(`Dry run — would apply ${file} (${Buffer.byteLength(markdown)} bytes).`);
644
+ const extra = local ? `, ${local.files.length} file${local.files.length === 1 ? "" : "s"} (${formatBytes(local.files.reduce((n, f) => n + f.size, 0))})` : "";
645
+ console.log(`Dry run — would apply ${file} (${Buffer.byteLength(markdown)} bytes${extra}).`);
646
+ for (const f of local?.files ?? [])
647
+ console.log(` ${f.path}`);
572
648
  return {};
573
649
  }
650
+ let uploaded = 0;
651
+ if (local?.files.length)
652
+ uploaded = await uploadPackFiles(config, local.files);
574
653
  let res;
575
654
  try {
576
- res = await (0, shared_1.apiRequest)("POST", "/api/cli/packs/apply", { config, body: { markdown } });
655
+ res = await (0, shared_1.apiRequest)("POST", "/api/cli/packs/apply", {
656
+ config,
657
+ body: {
658
+ markdown,
659
+ ...(local ? { files: local.files.map((f) => ({ path: f.path, sha256: f.sha256, ...(f.executable ? { executable: true } : {}) })) } : {}),
660
+ },
661
+ });
577
662
  }
578
663
  catch (error) {
579
664
  // The server reports pack.md problems with line numbers; show them as-is.
@@ -590,6 +675,8 @@ async function runApply(config, args) {
590
675
  const verb = res.created === true ? "Created" : res.changed === false ? "No changes to" : "Updated";
591
676
  const version = typeof res.version === "number" ? ` v${res.version}` : "";
592
677
  console.log(`✓ ${verb} ${slug}${version} from ${path.basename(file)}`);
678
+ if (local)
679
+ console.log(` files: ${local.files.length} (${uploaded} uploaded, ${local.files.length - uploaded} already on Codiedev)`);
593
680
  if (Array.isArray(res.warnings)) {
594
681
  for (const w of res.warnings) {
595
682
  const text = typeof w === "string" ? w : typeof w?.message === "string" ? `${w.agent ? `${w.agent}: ` : ""}${w.message}` : JSON.stringify(w);
@@ -599,4 +686,36 @@ async function runApply(config, args) {
599
686
  }
600
687
  return res;
601
688
  }
689
+ function formatBytes(n) {
690
+ return n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)}MB` : n >= 1024 ? `${Math.round(n / 1024)}KB` : `${n}B`;
691
+ }
692
+ /** Upload the blobs Codiedev doesn't have yet. Returns how many were sent. */
693
+ async function uploadPackFiles(config, files) {
694
+ const unique = [...new Map(files.map((f) => [f.sha256, f])).values()];
695
+ let known;
696
+ try {
697
+ known = (await (0, shared_1.apiRequest)("POST", "/api/cli/packs/files/known", { config, body: { sha256s: unique.map((f) => f.sha256) } })).known ?? [];
698
+ }
699
+ catch (error) {
700
+ const e = error;
701
+ if (e.status === 403)
702
+ throw new Error(`Only admins and team managers can add files to a pack (${e.message})`);
703
+ throw error;
704
+ }
705
+ const have = new Set(known);
706
+ let sent = 0;
707
+ for (const f of unique) {
708
+ if (have.has(f.sha256))
709
+ continue;
710
+ const body = fs.readFileSync(f.abs);
711
+ try {
712
+ await (0, shared_1.apiRequestBytes)("POST", "/api/cli/packs/files/upload", { config, body, query: { sha256: f.sha256 } });
713
+ }
714
+ catch (error) {
715
+ throw new Error(`couldn't upload ${f.path} (${error.message})`);
716
+ }
717
+ sent += 1;
718
+ }
719
+ return sent;
720
+ }
602
721
  exports.PACK_HELP = USAGE;
@@ -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>;