codiedev 0.9.86 → 0.9.88

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/codie.js CHANGED
@@ -50,6 +50,7 @@ const path = __importStar(require("path"));
50
50
  const child_process_1 = require("child_process");
51
51
  const url_1 = require("url");
52
52
  const configDir_1 = require("./configDir");
53
+ const utils_1 = require("./utils");
53
54
  const codieLauncher_1 = require("./codieLauncher");
54
55
  const codieBrandInstall_1 = require("./codieBrandInstall");
55
56
  const codieUsage_1 = require("./codieUsage");
@@ -274,12 +275,23 @@ async function main() {
274
275
  const capturePlugin = (0, codieLauncher_1.resolveCapturePlugin)({ homeDir: os.homedir(), existsSync: fs.existsSync });
275
276
  (0, codieBrandInstall_1.installTuiPlugin)();
276
277
  installBrandWordmark(runtime);
277
- const config = (0, codieLauncher_1.buildCodieConfig)({
278
+ // OpenCode doesn't read the instruction blocks `connect` writes for other
279
+ // tools, so Codie ships its own copy of the Codiedev commands.
280
+ const instructionsFile = path.join(stateDir, "codie-instructions.md");
281
+ try {
282
+ fs.mkdirSync(stateDir, { recursive: true });
283
+ fs.writeFileSync(instructionsFile, (0, utils_1.codiedevInstructionsText)(), "utf8");
284
+ }
285
+ catch (error) {
286
+ console.error(`codie: couldn't write the Codiedev instructions (${error instanceof Error ? error.message : error})`);
287
+ }
288
+ const baseConfig = (0, codieLauncher_1.buildCodieConfig)({
278
289
  existing: withStarterPacks(existingInlineConfig(), new Set(catalog.map((model) => (0, codieLauncher_1.bareModel)(model.id)).filter(Boolean)), catalog),
279
290
  catalog,
280
291
  env: { ...process.env, ...parsed.overrides, CODIEDEV_GATEWAY_URL: gatewayUrl },
281
292
  plugins: [capturePlugin, metadataPlugin],
282
293
  });
294
+ const config = fs.existsSync(instructionsFile) ? (0, codieLauncher_1.withCodiedevInstructions)(baseConfig, instructionsFile) : baseConfig;
283
295
  const env = {
284
296
  ...process.env,
285
297
  ...parsed.overrides,
@@ -33,6 +33,10 @@ export declare function usableModelLimit(value: unknown): ModelLimit | undefined
33
33
  */
34
34
  export declare function withCodieWorkingDirectory(args: string[], cwd: string): string[];
35
35
  export declare function bareModel(value: unknown): string;
36
+ /** Add the Codiedev instructions file to OpenCode's `instructions` (kept
37
+ * alongside the user's own, never duplicated). Codie writes the file itself:
38
+ * OpenCode never sees the instruction blocks `connect` writes for other tools. */
39
+ export declare function withCodiedevInstructions(config: Record<string, any>, file: string): Record<string, any>;
36
40
  export declare function buildCodieConfig({ existing, catalog, env, plugin, plugins, }: {
37
41
  existing?: Record<string, any>;
38
42
  catalog: CatalogModel[];
@@ -47,6 +47,7 @@ exports.usableModelCost = usableModelCost;
47
47
  exports.usableModelLimit = usableModelLimit;
48
48
  exports.withCodieWorkingDirectory = withCodieWorkingDirectory;
49
49
  exports.bareModel = bareModel;
50
+ exports.withCodiedevInstructions = withCodiedevInstructions;
50
51
  exports.buildCodieConfig = buildCodieConfig;
51
52
  exports.stageUsageModule = stageUsageModule;
52
53
  exports.withBrandPlugin = withBrandPlugin;
@@ -140,6 +141,13 @@ function normalizeAgentModels(agents, approved, fallback) {
140
141
  }
141
142
  return normalized;
142
143
  }
144
+ /** Add the Codiedev instructions file to OpenCode's `instructions` (kept
145
+ * alongside the user's own, never duplicated). Codie writes the file itself:
146
+ * OpenCode never sees the instruction blocks `connect` writes for other tools. */
147
+ function withCodiedevInstructions(config, file) {
148
+ const current = Array.isArray(config.instructions) ? config.instructions.filter((x) => typeof x === "string") : [];
149
+ return { ...config, instructions: current.includes(file) ? current : [...current, file] };
150
+ }
143
151
  function buildCodieConfig({ existing = {}, catalog, env = {}, plugin, plugins = [], }) {
144
152
  if (!Array.isArray(catalog) || catalog.length === 0) {
145
153
  throw new Error("No company-approved models were returned by Codiedev");
@@ -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 { LocalSkill } from "../packs/localSkills";
5
6
  import { PackBundleFile } from "../packs/files";
6
7
  export declare const DEFAULT_BACKEND_URL = "https://cli.codiedev.com";
7
8
  /** CODIEDEV_TOKEN alone is enough: no ~/.codiedev/config.json needed (CI).
@@ -67,4 +68,13 @@ export interface PublishResult {
67
68
  /** `codiedev pack publish <slug> [--channel stable|beta|alpha]`: snapshot the
68
69
  * draft onto a channel. Re-running with no changes publishes nothing. */
69
70
  export declare function runPublish(config: CodiedevConfig, args: PackArgs): Promise<PublishResult>;
70
- 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> [--publish] [--channel stable|beta|alpha] [--dry-run]\n Create or update a pack from a pack.md\n (--publish then publishes it, see below)\n codiedev pack publish <slug> [--channel stable|beta|alpha]\n Publish the pack's draft to a channel (default\n stable). Nothing happens when it hasn't changed.\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 Keep a pack up to date from its repo (GitLab; on merges to main). Use a\n personal Codiedev token of a pack owner (portal \u2192 Integrations \u2192 CLI tokens) as a\n masked CODIEDEV_TOKEN: pack CI keys can only install, not publish.\n pack-release:\n image: node:22\n rules: [{ if: '$CI_COMMIT_BRANCH == \"main\"' }]\n script:\n - npm i -g codiedev\n - codiedev pack apply packs/platform-starter.pack.md --publish\n This ships the pack's files, agents, hooks and skill list. Edits to a skill's\n own SKILL.md still go to the library with `codiedev push`.\n\n Or vendor the plugin into the repo instead of installing at build time:\n codiedev pack export <slug> --out .codiedev/packs";
71
+ /** The library key a skill name is stored under (server preferredSkillKey). */
72
+ export declare function skillKeyFor(name: string): string;
73
+ /** Push a local skill (and its folder's files) unless the library already
74
+ * has this exact text and files, so CI doesn't add a version on every merge.
75
+ * Team-visible: packs ship to teams. */
76
+ export declare function pushLocalSkill(config: CodiedevConfig, skill: LocalSkill): Promise<{
77
+ key: string;
78
+ pushed: boolean;
79
+ }>;
80
+ 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> [--publish] [--channel stable|beta|alpha] [--dry-run]\n Create or update a pack from a pack.md\n (--publish then publishes it, see below)\n codiedev pack publish <slug> [--channel stable|beta|alpha]\n Publish the pack's draft to a channel (default\n stable). Nothing happens when it hasn't changed.\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 Keep a pack up to date from its repo (GitLab; on merges to main). Use a\n personal Codiedev token of a pack owner (portal \u2192 Integrations \u2192 CLI tokens) as a\n masked CODIEDEV_TOKEN: pack CI keys can only install, not publish.\n pack-release:\n image: node:22\n rules: [{ if: '$CI_COMMIT_BRANCH == \"main\"' }]\n script:\n - npm i -g codiedev\n - codiedev pack apply packs/platform-starter.pack.md --publish\n Skills listed in pack.md as local folders (\"- ./skills/healthcare-scan\") are\n pushed when their SKILL.md changed, and the rest of each folder ships with it.\n\n Or vendor the plugin into the repo instead of installing at build time:\n codiedev pack export <slug> --out .codiedev/packs";
@@ -47,6 +47,8 @@ exports.exportPluginFolder = exportPluginFolder;
47
47
  exports.runExport = runExport;
48
48
  exports.runApply = runApply;
49
49
  exports.runPublish = runPublish;
50
+ exports.skillKeyFor = skillKeyFor;
51
+ exports.pushLocalSkill = pushLocalSkill;
50
52
  const install_1 = require("../packs/install");
51
53
  const harnesses_1 = require("../packs/harnesses");
52
54
  const manifest_1 = require("../packs/manifest");
@@ -60,6 +62,10 @@ const sync_1 = require("../packs/sync");
60
62
  const harnesses_2 = require("../packs/harnesses");
61
63
  const os = __importStar(require("os"));
62
64
  const utils_1 = require("../utils");
65
+ const push_1 = require("./push");
66
+ const skillFiles_1 = require("../packs/skillFiles");
67
+ const skillMarker_1 = require("../skillMarker");
68
+ const localSkills_1 = require("../packs/localSkills");
63
69
  const shared_1 = require("./shared");
64
70
  const files_1 = require("../packs/files");
65
71
  // --- CI (pack-scoped `cdvci_` keys) ------------------------------------------
@@ -170,8 +176,8 @@ got the pack. Skills from a pack are /<pack-slug>:<skill> in Claude Code.
170
176
  script:
171
177
  - npm i -g codiedev
172
178
  - codiedev pack apply packs/platform-starter.pack.md --publish
173
- This ships the pack's files, agents, hooks and skill list. Edits to a skill's
174
- own SKILL.md still go to the library with \`codiedev push\`.
179
+ Skills listed in pack.md as local folders ("- ./skills/healthcare-scan") are
180
+ pushed when their SKILL.md changed, and the rest of each folder ships with it.
175
181
 
176
182
  Or vendor the plugin into the repo instead of installing at build time:
177
183
  codiedev pack export <slug> --out .codiedev/packs`;
@@ -667,19 +673,46 @@ async function runApply(config, args) {
667
673
  }
668
674
  if (!markdown.trim())
669
675
  throw new Error(`${file} is empty`);
670
- // `## Files` (SIG-127): local folders/files to ship in the plugin dir.
671
- // Absent ⇒ the pack's files are left as they are.
676
+ const baseDir = path.dirname(file);
677
+ // SIG-128: local skills under `## Skills` are pushed; a skill folder's other
678
+ // contents (scripts/, docs/) travel with the skill itself, so every pack
679
+ // that includes it ships them under skills/<name>/.
680
+ const skills = (0, localSkills_1.findLocalSkills)(markdown, baseDir);
681
+ // `## Files` (SIG-127): shared folders/files for the plugin dir (a runner).
682
+ // No Files section ⇒ the pack's own files are left as they are.
672
683
  const entries = (0, files_1.parseFilesSection)(markdown);
673
- const local = entries ? (0, files_1.collectLocalFiles)(entries, path.dirname(file)) : null;
684
+ const local = entries ? (0, files_1.collectLocalFiles)(entries, baseDir) : null;
674
685
  for (const note of local?.notes ?? [])
675
686
  console.log(` · ${note}`);
676
687
  if (args.dryRun) {
677
688
  const extra = local ? `, ${local.files.length} file${local.files.length === 1 ? "" : "s"} (${formatBytes(local.files.reduce((n, f) => n + f.size, 0))})` : "";
678
689
  console.log(`Dry run — would apply ${file} (${Buffer.byteLength(markdown)} bytes${extra}).`);
690
+ for (const skill of skills) {
691
+ const folder = skill.dir ? (0, skillFiles_1.skillFolderFiles)(skill.skillFile) : null;
692
+ const files = folder && "files" in folder ? folder : null;
693
+ const withFiles = files?.files.length ? `, with ${files.files.length} file${files.files.length === 1 ? "" : "s"} from its folder` : "";
694
+ console.log(` skill ${skill.name} ← ${path.relative(baseDir, skill.skillFile) || skill.skillFile} (pushed if changed${withFiles})`);
695
+ if (folder && "skipped" in folder)
696
+ console.log(` ! SKILL.md only: ${folder.skipped}`);
697
+ for (const note of files?.notes ?? [])
698
+ console.log(` · ${note}`);
699
+ for (const f of files?.files ?? [])
700
+ console.log(` ${skill.pluginDir}/${f.path}`);
701
+ }
679
702
  for (const f of local?.files ?? [])
680
703
  console.log(` ${f.path}`);
681
704
  return {};
682
705
  }
706
+ const keys = new Map();
707
+ let pushedSkills = 0;
708
+ for (const skill of skills) {
709
+ const { key, pushed } = await pushLocalSkill(config, skill);
710
+ keys.set(skill.line, key);
711
+ if (pushed)
712
+ pushedSkills += 1;
713
+ }
714
+ if (keys.size)
715
+ markdown = (0, localSkills_1.rewriteLocalSkills)(markdown, keys);
683
716
  let uploaded = 0;
684
717
  if (local?.files.length)
685
718
  uploaded = await uploadPackFiles(config, local.files);
@@ -708,6 +741,8 @@ async function runApply(config, args) {
708
741
  const verb = res.created === true ? "Created" : res.changed === false ? "No changes to" : "Updated";
709
742
  const version = typeof res.version === "number" ? ` v${res.version}` : "";
710
743
  console.log(`✓ ${verb} ${slug}${version} from ${path.basename(file)}`);
744
+ if (skills.length)
745
+ console.log(` skills: ${skills.length} local (${pushedSkills} pushed, ${skills.length - pushedSkills} unchanged)`);
711
746
  if (local)
712
747
  console.log(` files: ${local.files.length} (${uploaded} uploaded, ${local.files.length - uploaded} already on Codiedev)`);
713
748
  if (Array.isArray(res.warnings)) {
@@ -762,15 +797,63 @@ async function runPublish(config, args) {
762
797
  console.log(`✓ Published ${out.slug} v${out.version} to ${channel}. Teams on ${channel} get it at their next sync.`);
763
798
  return out;
764
799
  }
800
+ /** The library key a skill name is stored under (server preferredSkillKey). */
801
+ function skillKeyFor(name) {
802
+ const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64).replace(/-+$/g, "");
803
+ return slug ? `${slug}.md` : name;
804
+ }
805
+ /** Push a local skill (and its folder's files) unless the library already
806
+ * has this exact text and files, so CI doesn't add a version on every merge.
807
+ * Team-visible: packs ship to teams. */
808
+ async function pushLocalSkill(config, skill) {
809
+ const local = (0, skillMarker_1.stripSkillMarker)(fs.readFileSync(skill.skillFile, "utf8"));
810
+ const found = skill.dir ? (0, skillFiles_1.skillFolderFiles)(skill.skillFile) : null;
811
+ if (found && "skipped" in found)
812
+ console.log(` ! ${skill.name}: pushing SKILL.md only, ${found.skipped}. The skill keeps the files it had.`);
813
+ const folder = found && "files" in found ? found : null;
814
+ for (const note of folder?.notes ?? [])
815
+ console.log(` · ${skill.name}: ${note}`);
816
+ const localRefs = folder ? folder.files.map(skillFiles_1.toRef) : undefined;
817
+ for (const key of [skillKeyFor(skill.name), skill.name]) {
818
+ try {
819
+ const current = await (0, shared_1.apiRequest)("GET", "/api/cli/pull", { config, query: { key } });
820
+ const remote = current.artifact?.markdown;
821
+ if (current.artifact?.kind === "skill" &&
822
+ typeof remote === "string" &&
823
+ (0, skillMarker_1.stripSkillMarker)(remote).trimEnd() === local.trimEnd() &&
824
+ (!localRefs || (0, skillFiles_1.sameFileRefs)(current.artifact.files, localRefs))) {
825
+ return { key: current.artifact.key ?? key, pushed: false };
826
+ }
827
+ break;
828
+ }
829
+ catch (error) {
830
+ if (error.status !== 404)
831
+ throw new Error(`couldn't check skill ${skill.name} (${error.message})`);
832
+ }
833
+ }
834
+ try {
835
+ if (folder)
836
+ await (0, skillFiles_1.uploadBlobs)(config, folder.files, "/api/cli/skills/files");
837
+ const res = await (0, push_1.pushFile)(config, {
838
+ absPath: skill.skillFile,
839
+ key: path.basename(skill.skillFile),
840
+ kind: "skill",
841
+ visibility: "team",
842
+ ...(localRefs ? { files: localRefs } : {}),
843
+ });
844
+ return { key: res.key ?? skill.name, pushed: true };
845
+ }
846
+ catch (error) {
847
+ throw new Error(`couldn't push skill ${skill.name} (${error.message})`);
848
+ }
849
+ }
765
850
  function formatBytes(n) {
766
851
  return n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)}MB` : n >= 1024 ? `${Math.round(n / 1024)}KB` : `${n}B`;
767
852
  }
768
- /** Upload the blobs Codiedev doesn't have yet. Returns how many were sent. */
853
+ /** Upload the pack's own blobs Codiedev doesn't have yet. Returns how many were sent. */
769
854
  async function uploadPackFiles(config, files) {
770
- const unique = [...new Map(files.map((f) => [f.sha256, f])).values()];
771
- let known;
772
855
  try {
773
- known = (await (0, shared_1.apiRequest)("POST", "/api/cli/packs/files/known", { config, body: { sha256s: unique.map((f) => f.sha256) } })).known ?? [];
856
+ return await (0, skillFiles_1.uploadBlobs)(config, files, "/api/cli/packs/files");
774
857
  }
775
858
  catch (error) {
776
859
  const e = error;
@@ -778,20 +861,5 @@ async function uploadPackFiles(config, files) {
778
861
  throw new Error(`Only admins and team managers can add files to a pack (${e.message})`);
779
862
  throw error;
780
863
  }
781
- const have = new Set(known);
782
- let sent = 0;
783
- for (const f of unique) {
784
- if (have.has(f.sha256))
785
- continue;
786
- const body = fs.readFileSync(f.abs);
787
- try {
788
- await (0, shared_1.apiRequestBytes)("POST", "/api/cli/packs/files/upload", { config, body, query: { sha256: f.sha256 } });
789
- }
790
- catch (error) {
791
- throw new Error(`couldn't upload ${f.path} (${error.message})`);
792
- }
793
- sent += 1;
794
- }
795
- return sent;
796
864
  }
797
865
  exports.PACK_HELP = USAGE;
@@ -1,4 +1,5 @@
1
1
  import { requireConfig } from "./shared";
2
+ import { FileRef } from "../packs/skillFiles";
2
3
  interface PushResponse {
3
4
  ok: boolean;
4
5
  artifactId: string;
@@ -17,6 +18,8 @@ export interface ParsedArgs {
17
18
  force: boolean;
18
19
  as?: string;
19
20
  private?: boolean;
21
+ /** --no-files: push SKILL.md only; the skill keeps its current files. */
22
+ noFiles?: boolean;
20
23
  }
21
24
  export declare function parseArgs(args: string[]): ParsedArgs;
22
25
  export declare function pushFile(config: ReturnType<typeof requireConfig>, opts: {
@@ -28,6 +31,17 @@ export declare function pushFile(config: ReturnType<typeof requireConfig>, opts:
28
31
  kind?: string;
29
32
  tags?: string[];
30
33
  force?: boolean;
34
+ /** A skill folder's other files, already uploaded (uploadBlobs). Omitted =
35
+ * the new version keeps the previous version's files. */
36
+ files?: FileRef[];
31
37
  }): Promise<PushResponse>;
38
+ /** For a SKILL.md in its own folder: upload the folder's other files and
39
+ * return their refs. null = send no list (not a skill folder, or one over the
40
+ * limits, which is warned about): the skill keeps its current files. */
41
+ export declare function uploadSkillFolder(config: ReturnType<typeof requireConfig>, skillFile: string, log?: (line: string) => void): Promise<{
42
+ files: FileRef[];
43
+ bytes: number;
44
+ } | null>;
45
+ export declare function formatBytes(n: number): string;
32
46
  export declare function runPush(args: string[]): Promise<void>;
33
47
  export {};
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.parseArgs = parseArgs;
37
37
  exports.pushFile = pushFile;
38
+ exports.uploadSkillFolder = uploadSkillFolder;
39
+ exports.formatBytes = formatBytes;
38
40
  exports.runPush = runPush;
39
41
  const fs = __importStar(require("fs"));
40
42
  const skillMarker_1 = require("../skillMarker");
@@ -42,6 +44,7 @@ const path = __importStar(require("path"));
42
44
  const shared = __importStar(require("./shared"));
43
45
  const shared_1 = require("./shared");
44
46
  const utils_1 = require("../utils");
47
+ const skillFiles_1 = require("../packs/skillFiles");
45
48
  const VALID_TYPES = new Set([
46
49
  "spec",
47
50
  "bugfix",
@@ -58,6 +61,7 @@ function parseArgs(args) {
58
61
  let force = false;
59
62
  let as;
60
63
  let priv = false;
64
+ let noFiles = false;
61
65
  const tags = [];
62
66
  for (let i = 0; i < args.length; i++) {
63
67
  const a = args[i];
@@ -91,12 +95,15 @@ function parseArgs(args) {
91
95
  else if (a === "--private") {
92
96
  priv = true;
93
97
  }
98
+ else if (a === "--no-files") {
99
+ noFiles = true;
100
+ }
94
101
  else if (!a.startsWith("--") && !file) {
95
102
  file = a;
96
103
  }
97
104
  }
98
105
  if (!file) {
99
- console.error("Usage: codiedev push <file.md> [--as <key>] [--kind doc|skill] [--tag <name>] [--private] [--force]");
106
+ console.error("Usage: codiedev push <file.md> [--as <key>] [--kind doc|skill] [--tag <name>] [--private] [--force] [--no-files]");
100
107
  process.exit(1);
101
108
  }
102
109
  if (type && !VALID_TYPES.has(type)) {
@@ -118,7 +125,7 @@ function parseArgs(args) {
118
125
  process.exit(1);
119
126
  }
120
127
  }
121
- return { file, type, kind, tags, force, as, private: priv };
128
+ return { file, type, kind, tags, force, as, private: priv, noFiles };
122
129
  }
123
130
  async function pushFile(config, opts) {
124
131
  // Never upload another artifact's identity marker (skillMarker.ts).
@@ -136,11 +143,31 @@ async function pushFile(config, opts) {
136
143
  gitRemoteUrl,
137
144
  visibility: opts.visibility,
138
145
  contentHash: opts.contentHash,
146
+ ...(opts.files ? { files: opts.files } : {}),
139
147
  },
140
148
  });
141
149
  }
150
+ /** For a SKILL.md in its own folder: upload the folder's other files and
151
+ * return their refs. null = send no list (not a skill folder, or one over the
152
+ * limits, which is warned about): the skill keeps its current files. */
153
+ async function uploadSkillFolder(config, skillFile, log = () => { }) {
154
+ const folder = (0, skillFiles_1.skillFolderFiles)(skillFile);
155
+ if (!folder)
156
+ return null;
157
+ if ("skipped" in folder) {
158
+ log(` ! Pushing SKILL.md only: ${folder.skipped}. The skill keeps the files it had.`);
159
+ return null;
160
+ }
161
+ for (const note of folder.notes)
162
+ log(` · ${note}`);
163
+ await (0, skillFiles_1.uploadBlobs)(config, folder.files, "/api/cli/skills/files");
164
+ return { files: folder.files.map(skillFiles_1.toRef), bytes: folder.bytes };
165
+ }
166
+ function formatBytes(n) {
167
+ return n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)}MB` : n >= 1024 ? `${Math.round(n / 1024)}KB` : `${n}B`;
168
+ }
142
169
  async function runPush(args) {
143
- const { file, type, kind, tags, force, as, private: priv } = parseArgs(args);
170
+ const { file, type, kind, tags, force, as, private: priv, noFiles } = parseArgs(args);
144
171
  const config = (0, shared_1.requireConfig)();
145
172
  let absolute = file;
146
173
  if (!path.isAbsolute(absolute)) {
@@ -162,6 +189,8 @@ async function runPush(args) {
162
189
  const filename = as ?? path.basename(absolute);
163
190
  const start = Date.now();
164
191
  try {
192
+ // A skill's folder (scripts/, docs/, PDFs) travels with it.
193
+ const folder = !noFiles && kind !== "doc" ? await uploadSkillFolder(config, absolute, (l) => console.log(l)) : null;
165
194
  const res = await pushFile(config, {
166
195
  absPath: absolute,
167
196
  key: filename,
@@ -170,6 +199,7 @@ async function runPush(args) {
170
199
  tags,
171
200
  force,
172
201
  visibility: priv ? "private" : undefined,
202
+ ...(folder ? { files: folder.files } : {}),
173
203
  });
174
204
  (0, shared_1.recordAgentEvent)(config, {
175
205
  tool: "codiedev_push",
@@ -177,7 +207,10 @@ async function runPush(args) {
177
207
  latencyMs: Date.now() - start,
178
208
  });
179
209
  const tagSummary = res.tags && res.tags.length > 0 ? ` tags=[${res.tags.join(", ")}]` : "";
180
- console.log(`✓ Pushed ${filename} (kind=${res.kind}${tagSummary}).`);
210
+ const fileSummary = folder?.files.length
211
+ ? `, with ${folder.files.length} file${folder.files.length === 1 ? "" : "s"} from its folder, ${formatBytes(folder.bytes)}`
212
+ : "";
213
+ console.log(`✓ Pushed ${filename} (kind=${res.kind}${tagSummary}${fileSummary}).`);
181
214
  console.log(` Visible under: ${res.destination}`);
182
215
  // Echo the *resolved* key the server stored, not the local filename —
183
216
  // skills are keyed by their frontmatter name (so two `SKILL.md` files
package/dist/managedPi.js CHANGED
@@ -230,6 +230,9 @@ function prepareManagedPiDir(opts) {
230
230
  // extensions dropped into the dir go away on every launch.
231
231
  fs.rmSync(path.join(agentDir, "auth.json"), { force: true });
232
232
  fs.rmSync(path.join(agentDir, "extensions"), { recursive: true, force: true });
233
+ // pi reads <agentDir>/AGENTS.md as global context; the managed dir is ours,
234
+ // so the Codiedev instructions are written fresh on every launch.
235
+ fs.writeFileSync(path.join(agentDir, "AGENTS.md"), (0, utils_1.codiedevInstructionsText)(), { mode: 0o600 });
233
236
  const guard = path.join(agentDir, exports.GUARD_FILE);
234
237
  fs.writeFileSync(guard, exports.GUARD_SOURCE, { mode: 0o600 });
235
238
  const capture = path.join(agentDir, exports.CAPTURE_FILE);
@@ -36,9 +36,13 @@ export declare function expandHome(p: string, home?: string): string;
36
36
  /** Walk each `## Files` entry. Junk (caches, VCS, editor files) is skipped
37
37
  * quietly; symlinks, hidden files and a skill folder's own SKILL.md are
38
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): {
39
+ export declare function collectLocalFiles(entries: FilesEntry[], baseDir: string, home?: string, opts?: {
40
+ lenient?: boolean;
41
+ checkAs?: (dest: string) => string;
42
+ }): {
40
43
  files: LocalFile[];
41
44
  notes: string[];
45
+ total: number;
42
46
  };
43
47
  /** Only Claude Code sets ${CLAUDE_PLUGIN_ROOT}. In the copy other tools read,
44
48
  * replace it with the plugin dir's real path (forward slashes, which every
@@ -218,12 +218,15 @@ function expandHome(p, home = os.homedir()) {
218
218
  /** Walk each `## Files` entry. Junk (caches, VCS, editor files) is skipped
219
219
  * quietly; symlinks, hidden files and a skill folder's own SKILL.md are
220
220
  * skipped with a note. Throws on a missing source or a bad destination. */
221
- function collectLocalFiles(entries, baseDir, home = os.homedir()) {
221
+ function collectLocalFiles(entries, baseDir, home = os.homedir(), opts = {}) {
222
222
  const files = new Map();
223
223
  const notes = [];
224
224
  let total = 0;
225
225
  const add = (dest, abs) => {
226
- const problem = packFilePathProblem(dest);
226
+ // A skill folder's own SKILL.md is the skill itself (pushed separately).
227
+ if (opts.lenient && dest.toLowerCase() === "skill.md")
228
+ return;
229
+ const problem = packFilePathProblem(opts.checkAs ? opts.checkAs(dest) : dest);
227
230
  if (problem) {
228
231
  const parts = dest.split("/");
229
232
  if (parts.length === 3 && parts[0] === "skills" && parts[2].toLowerCase() === "skill.md") {
@@ -234,9 +237,18 @@ function collectLocalFiles(entries, baseDir, home = os.homedir()) {
234
237
  notes.push(`skipped hidden file ${dest}`);
235
238
  return;
236
239
  }
240
+ if (opts.lenient) {
241
+ // A skill folder: skip what can't ship, never fail the push.
242
+ notes.push(`skipped ${dest} (${problem})`);
243
+ return;
244
+ }
237
245
  throw new Error(`can't ship ${dest}: ${problem}`);
238
246
  }
239
247
  const stat = fs.statSync(abs);
248
+ if (stat.size > exports.PACK_FILE_MAX_BYTES && opts.lenient) {
249
+ notes.push(`skipped ${dest} (over ${exports.PACK_FILE_MAX_BYTES / (1024 * 1024)}MB)`);
250
+ return;
251
+ }
240
252
  if (stat.size > exports.PACK_FILE_MAX_BYTES)
241
253
  throw new Error(`${abs} is over ${exports.PACK_FILE_MAX_BYTES / (1024 * 1024)}MB`);
242
254
  const key = dest.toLowerCase();
@@ -291,11 +303,13 @@ function collectLocalFiles(entries, baseDir, home = os.homedir()) {
291
303
  throw new Error(`pack.md line ${entry.line}: ${entry.source} is not a file or folder`);
292
304
  }
293
305
  const list = [...files.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
306
+ if (opts.lenient)
307
+ return { files: list, notes, total };
294
308
  if (list.length > exports.PACK_MAX_FILES)
295
309
  throw new Error(`a pack holds at most ${exports.PACK_MAX_FILES} files (found ${list.length})`);
296
310
  if (total > exports.PACK_FILES_MAX_TOTAL_BYTES)
297
311
  throw new Error(`pack files total over ${exports.PACK_FILES_MAX_TOTAL_BYTES / (1024 * 1024)}MB`);
298
- return { files: list, notes };
312
+ return { files: list, notes, total };
299
313
  }
300
314
  // ─── Portable copies ────────────────────────────────────────────────────────
301
315
  /** Only Claude Code sets ${CLAUDE_PLUGIN_ROOT}. In the copy other tools read,
@@ -0,0 +1,18 @@
1
+ import { FilesEntry } from "./files";
2
+ export interface LocalSkill {
3
+ /** 1-based line in pack.md. */
4
+ line: number;
5
+ source: string;
6
+ skillFile: string;
7
+ /** The skill folder, when the entry named one (its files ship with the skill). */
8
+ dir: string | null;
9
+ name: string;
10
+ /** `skills/<dir>/` in the plugin, matching the server's skillDirName. */
11
+ pluginDir: string;
12
+ }
13
+ /** Local-path lines under `## Skills` (fenced blocks are skipped). */
14
+ export declare function findLocalSkills(markdown: string, baseDir: string, home?: string): LocalSkill[];
15
+ /** pack.md with each local skill line replaced by `- <key> @me`. */
16
+ export declare function rewriteLocalSkills(markdown: string, keys: Map<number, string>): string;
17
+ /** Files entries for skill folders, so their scripts/docs ship with them. */
18
+ export declare function skillFolderEntries(skills: LocalSkill[]): FilesEntry[];
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.findLocalSkills = findLocalSkills;
37
+ exports.rewriteLocalSkills = rewriteLocalSkills;
38
+ exports.skillFolderEntries = skillFolderEntries;
39
+ // SIG-128: local skills in a pack.md. Under `## Skills`, a line naming a local
40
+ // path (`./…`, `../…`, `~/…`, `/…`) instead of `<key> @<author email>` is a
41
+ // skill on this machine: `pack apply` pushes it to the library (only when it
42
+ // changed) and rewrites the line to `<key> @me`. A skill FOLDER also ships
43
+ // the rest of its contents (scripts/, docs/…) as the skill's pack files.
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
47
+ const files_1 = require("./files");
48
+ const manifest_1 = require("./manifest");
49
+ const LOCAL = /^(\.{1,2}\/|~\/|~$|\/)/;
50
+ /** Same rule as the server's skillDirName: the frontmatter name when it's a
51
+ * safe folder name, else a slug of it. */
52
+ function pluginDirName(name) {
53
+ if (/^[A-Za-z0-9][A-Za-z0-9._ -]{0,127}$/.test(name) && !name.includes(".."))
54
+ return name;
55
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/g, "");
56
+ return slug || "pack";
57
+ }
58
+ /** Local-path lines under `## Skills` (fenced blocks are skipped). */
59
+ function findLocalSkills(markdown, baseDir, home = os.homedir()) {
60
+ const lines = markdown.replace(/^/, "").split("\n").map((l) => l.replace(/\r$/, ""));
61
+ const out = [];
62
+ let fence = null;
63
+ let inSkills = false;
64
+ for (let i = 0; i < lines.length; i++) {
65
+ const line = lines[i];
66
+ const f = line.match(/^(`{3,})/);
67
+ if (f) {
68
+ if (!fence)
69
+ fence = f[1];
70
+ else if (f[1].length >= fence.length && /^`+\s*$/.test(line))
71
+ fence = null;
72
+ continue;
73
+ }
74
+ if (fence)
75
+ continue;
76
+ const h2 = line.match(/^##\s+(.+?)\s*$/);
77
+ if (h2 && !line.startsWith("###")) {
78
+ inSkills = h2[1].toLowerCase() === "skills";
79
+ continue;
80
+ }
81
+ if (!inSkills)
82
+ continue;
83
+ const m = line.match(/^\s*[-*]\s+(.+?)\s*$/);
84
+ if (!m)
85
+ continue;
86
+ const raw = m[1].replace(/^["']|["']$/g, "");
87
+ if (!LOCAL.test(raw))
88
+ continue;
89
+ const abs = path.resolve(baseDir, (0, files_1.expandHome)(raw, home));
90
+ let stat;
91
+ try {
92
+ stat = fs.statSync(abs);
93
+ }
94
+ catch {
95
+ throw new Error(`pack.md line ${i + 1}: ${raw} doesn't exist`);
96
+ }
97
+ const dir = stat.isDirectory() ? abs : null;
98
+ const skillFile = dir ? path.join(dir, "SKILL.md") : abs;
99
+ if (!fs.existsSync(skillFile))
100
+ throw new Error(`pack.md line ${i + 1}: no SKILL.md in ${raw}`);
101
+ const { data } = (0, manifest_1.parseFrontmatter)(fs.readFileSync(skillFile, "utf8"));
102
+ const name = (data.name ?? "").trim();
103
+ if (!name || !(data.description ?? "").trim()) {
104
+ throw new Error(`pack.md line ${i + 1}: ${path.relative(baseDir, skillFile) || skillFile} needs SKILL.md frontmatter with name: and description:`);
105
+ }
106
+ out.push({ line: i + 1, source: raw, skillFile, dir, name, pluginDir: `skills/${pluginDirName(name)}` });
107
+ }
108
+ const seen = new Map();
109
+ for (const skill of out) {
110
+ const prior = seen.get(skill.name.toLowerCase());
111
+ if (prior)
112
+ throw new Error(`pack.md lines ${prior.line} and ${skill.line} both add a skill named "${skill.name}"`);
113
+ seen.set(skill.name.toLowerCase(), skill);
114
+ }
115
+ return out;
116
+ }
117
+ /** pack.md with each local skill line replaced by `- <key> @me`. */
118
+ function rewriteLocalSkills(markdown, keys) {
119
+ const lines = markdown.split("\n");
120
+ for (const [line, key] of keys) {
121
+ const cr = lines[line - 1].endsWith("\r") ? "\r" : "";
122
+ const indent = lines[line - 1].match(/^\s*/)?.[0] ?? "";
123
+ lines[line - 1] = `${indent}- ${key} @me${cr}`;
124
+ }
125
+ return lines.join("\n");
126
+ }
127
+ /** Files entries for skill folders, so their scripts/docs ship with them. */
128
+ function skillFolderEntries(skills) {
129
+ return skills.filter((s) => s.dir).map((s) => ({ dest: s.pluginDir, source: s.dir, line: s.line }));
130
+ }
@@ -0,0 +1,37 @@
1
+ import { CodiedevConfig } from "../utils";
2
+ import { LocalFile } from "./files";
3
+ export type FileRef = {
4
+ path: string;
5
+ sha256: string;
6
+ executable?: boolean;
7
+ };
8
+ export declare const toRef: (f: {
9
+ path: string;
10
+ sha256: string;
11
+ executable?: boolean;
12
+ }) => FileRef;
13
+ export type SkillFolder = {
14
+ files: LocalFile[];
15
+ notes: string[];
16
+ bytes: number;
17
+ }
18
+ /** A skill folder that can't ship (over the limits): send no list, so the
19
+ * skill keeps its current files, and warn. */
20
+ | {
21
+ skipped: string;
22
+ };
23
+ /** The other files in a SKILL.md's folder, paths relative to that folder.
24
+ * null when the file isn't a skill in its own folder (a repo root or the home
25
+ * folder is never walked). Files that can't ship are skipped with a note, so
26
+ * the folder never fails a push. */
27
+ export declare function skillFolderFiles(skillFile: string, home?: string): SkillFolder | null;
28
+ /** Same files (path, content, executable bit), order-insensitive? */
29
+ export declare function sameFileRefs(a: FileRef[] | undefined, b: FileRef[] | undefined): boolean;
30
+ /** Upload the blobs Codiedev doesn't have yet. `base` is
31
+ * /api/cli/packs/files (admins and managers) or /api/cli/skills/files (anyone).
32
+ * Returns how many were sent. */
33
+ export declare function uploadBlobs(config: CodiedevConfig, files: Array<{
34
+ path: string;
35
+ sha256: string;
36
+ abs: string;
37
+ }>, base: "/api/cli/packs/files" | "/api/cli/skills/files"): Promise<number>;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.toRef = void 0;
37
+ exports.skillFolderFiles = skillFolderFiles;
38
+ exports.sameFileRefs = sameFileRefs;
39
+ exports.uploadBlobs = uploadBlobs;
40
+ // A skill's folder files: everything beside SKILL.md (scripts/, docs/, PDFs)
41
+ // travels with the skill. `codiedev push` of a SKILL.md uploads them as blobs
42
+ // and sends the list with the push; packs then ship them under
43
+ // skills/<name>/ automatically. Blobs share the pack-file store (files.ts).
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
47
+ const shared_1 = require("../commands/shared");
48
+ const files_1 = require("./files");
49
+ const manifest_1 = require("./manifest");
50
+ const toRef = (f) => ({
51
+ path: f.path,
52
+ sha256: f.sha256,
53
+ ...(f.executable ? { executable: true } : {}),
54
+ });
55
+ exports.toRef = toRef;
56
+ /** The other files in a SKILL.md's folder, paths relative to that folder.
57
+ * null when the file isn't a skill in its own folder (a repo root or the home
58
+ * folder is never walked). Files that can't ship are skipped with a note, so
59
+ * the folder never fails a push. */
60
+ function skillFolderFiles(skillFile, home = os.homedir()) {
61
+ const abs = path.resolve(skillFile);
62
+ if (path.basename(abs).toLowerCase() !== "skill.md")
63
+ return null;
64
+ // Only a real skill (name: + description:) carries files.
65
+ let data;
66
+ try {
67
+ data = (0, manifest_1.parseFrontmatter)(fs.readFileSync(abs, "utf8")).data;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ if (!(data.name ?? "").trim() || !(data.description ?? "").trim())
73
+ return null;
74
+ const dir = path.dirname(abs);
75
+ if (dir === path.resolve(home) || dir === path.parse(dir).root || fs.existsSync(path.join(dir, ".git")))
76
+ return null;
77
+ let walked;
78
+ try {
79
+ // Checked as the server does: as if under skills/<name>/ in a pack.
80
+ walked = (0, files_1.collectLocalFiles)([{ dest: ".", source: dir, line: 0 }], dir, home, { lenient: true, checkAs: (p) => `skills/s/${p}` });
81
+ }
82
+ catch (error) {
83
+ return { skipped: `couldn't read its folder (${error.message})` };
84
+ }
85
+ const files = walked.files.filter((f) => f.path.toLowerCase() !== "skill.md");
86
+ const bytes = files.reduce((n, f) => n + f.size, 0);
87
+ if (files.length > files_1.PACK_MAX_FILES || bytes > files_1.PACK_FILES_MAX_TOTAL_BYTES) {
88
+ return {
89
+ skipped: `its folder is over the limit (${files.length} files, ${(bytes / (1024 * 1024)).toFixed(1)}MB; at most ${files_1.PACK_MAX_FILES} files and ${files_1.PACK_FILES_MAX_TOTAL_BYTES / (1024 * 1024)}MB)`,
90
+ };
91
+ }
92
+ return { files, notes: walked.notes, bytes };
93
+ }
94
+ /** Same files (path, content, executable bit), order-insensitive? */
95
+ function sameFileRefs(a, b) {
96
+ const key = (l) => JSON.stringify((l ?? []).map((f) => [f.path, f.sha256, !!f.executable]).sort());
97
+ return key(a) === key(b);
98
+ }
99
+ /** Upload the blobs Codiedev doesn't have yet. `base` is
100
+ * /api/cli/packs/files (admins and managers) or /api/cli/skills/files (anyone).
101
+ * Returns how many were sent. */
102
+ async function uploadBlobs(config, files, base) {
103
+ const unique = [...new Map(files.map((f) => [f.sha256, f])).values()];
104
+ if (!unique.length)
105
+ return 0;
106
+ const have = new Set((await (0, shared_1.apiRequest)("POST", `${base}/known`, { config, body: { sha256s: unique.map((f) => f.sha256) } })).known ?? []);
107
+ let sent = 0;
108
+ for (const f of unique) {
109
+ if (have.has(f.sha256))
110
+ continue;
111
+ try {
112
+ await (0, shared_1.apiRequestBytes)("POST", `${base}/upload`, { config, body: fs.readFileSync(f.abs), query: { sha256: f.sha256 } });
113
+ }
114
+ catch (error) {
115
+ throw new Error(`couldn't upload ${f.path} (${error.message})`);
116
+ }
117
+ sent += 1;
118
+ }
119
+ return sent;
120
+ }
package/dist/utils.d.ts CHANGED
@@ -143,6 +143,11 @@ export declare function ensureAllCodiedevHooksInstalled(): void;
143
143
  export declare function hasDirectHookPath(configText: string): boolean;
144
144
  export declare function migrateHooksToLauncher(): string[];
145
145
  export declare function ensureSessionStartHookInstalled(): void;
146
+ export declare const CODIEDEV_INSTRUCTIONS_BODY = "## Codiedev \u2014 team artifact layer\n\nThis machine is connected to Codiedev. Teammates share specs, reviews,\ndecisions, and messages via a CLI on every dev's machine.\n\n**When the user asks to share, push, ping, check messages, or capture a\nthought, use the `codiedev` CLI via Bash:**\n\n| Intent | Command |\n|---|---|\n| \"push this spec\" / \"save this as a skill\" | `codiedev push <file.md>` |\n| \"pull X\" / \"grab the latest spec-X\" / \"what did a teammate push?\" | `codiedev pull <key>` |\n| \"delete X\" / \"remove that test push\" / \"rm spec-old\" | `codiedev delete <key>` (always confirm with the user first) |\n| \"ping a teammate about this\" / \"ask someone for their take\" / \"send them this file\" | `codiedev ping <name> \"<msg>\" [--with <key>] [--file <path>]` |\n| \"any messages?\" / \"check my inbox\" / \"what did a teammate say?\" | `codiedev inbox` |\n| \"read that reply\" / \"mark that ping read\" | `codiedev read <ping-id>` |\n| \"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) |\n| \"note that X is a follow-up\" / \"remember X\" | `codiedev note \"<text>\"` |\n| \"share with a teammate\" / \"give someone access\" (no notification) | `codiedev share <key> <name> [--role read\\|edit]` |\n| \"send this to a teammate\" / \"ask someone to look at this\" | `codiedev send <key> <name> [\"<msg>\"]` |\n| \"find something about X\" / \"has anyone solved Y?\" | `codiedev search \"<query>\" [--limit N]` |\n| \"show my library\" / \"what artifacts exist?\" | `codiedev library [--scope mine\\|shared\\|all] [--kind doc\\|skill]` |\n| \"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>`) |\n| \"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) |\n| \"add a helpfulness 1\u20135 score\" / \"make a review queue for tone\" | `codiedev traces score-kinds add <name> --type number --min 1 --max 5`; `codiedev traces queue create <name> --scores <a,b>` |\n| \"score that run 4 for helpfulness\" / \"how is my agent scoring?\" | `codiedev traces score <run> <score> <value> [--agent <name>] [--comment <why>]`; `codiedev traces scores <agent>` |\n| \"put this week's failed runs in the review queue\" | `codiedev traces queue list`; `codiedev traces queue add <queue> --agent <name> [--failed] [--days n]` |\n| \"have a judge score 10% of my agent's runs\" / \"turn off automatic scoring\" | `codiedev traces judge <agent> --on --scores <a,b> --sample 10 [--budget 25]`; `--off`; no flags shows it |\n| \"import these test cases from a CSV\" / \"add an example to the dataset\" / \"save the dataset as a version\" | `codiedev traces import <agent> <file> --dataset <name>`; `codiedev traces add-example <agent> --dataset <name> --input \"\u2026\" [--expected \"\u2026\"]`; `codiedev traces save-version <dataset>`; `codiedev traces versions <dataset>` |\n| \"make a starter pack\" / \"add this to our pack\" / \"package these skills for my team\" | write a `<slug>.pack.md` (start from `codiedev pack export <slug> --manifest --out .` for an existing pack), then `codiedev pack apply <file> [--publish]` \u2014 see **Starter packs** below |\n| \"publish the pack\" / \"ship the pack to my team\" | `codiedev pack publish <slug> [--channel stable\\|beta\\|alpha]` |\n| \"what was the last eval on X?\" / \"file these eval results\" | `codiedev evals last <subject>` before starting a related eval; `codiedev evals report <run.json>` after each variant runs (scores only \u2014 `codiedev evals help` for the shape) |\n\n**Starter packs** (team managers and admins): a pack.md is frontmatter\n(`name:`, `slug:`, optional `description:`) then sections. Ask the user\nwhat to include, write the file, run `codiedev pack apply <file> --dry-run`\nto show them, then apply for real:\n\n```\n---\nname: \"Healthcare scan\"\nslug: \"healthcare-scan\"\n---\n\n# Healthcare scan\n\n## Skills\n\n- ./skills/healthcare-scan\n- lint.md @teammate@example.com\n\n## Files\n\n- runner <- ~/.claude/runner\n```\n\nUnder **Skills**, a local path (`./\u2026`, `~/\u2026` or `/\u2026`) to a skill folder\nor SKILL.md is pushed to the library and added, with the rest of its folder\nshipped as the skill's files; `<key> @<author email>` adds a teammate's\nlibrary skill. **Files** maps local folders to paths in the pack\n(`.` is the pack root). In skills, point at shipped files relative to the\nskill's folder or with `${CLAUDE_PLUGIN_ROOT}/\u2026`. Applying changes the\ndraft; `--publish` (or `codiedev pack publish`) sends it to the team.\n\n**Just push the file \u2014 we'll figure out where it goes.** Every artifact\nis classified into one of two kinds at upload time:\n\n- **skill** \u2014 the file has SKILL.md frontmatter (`name:` + `description:`\n inside a `---` block). Shows on the Skills page in the portal and is\n invokable as a slash command (`/<name>`).\n- **doc** \u2014 everything else. Lives in the library: list it with\n `codiedev library`, fetch it with `codiedev pull <key>`.\n\nDetection is content-based, not filename-based. The filename prefix\n(`spec-` / `decision-` / `proposal-` / `bugfix-` / `review-` /\n`note-`) is preserved as a *tag* you can filter by, but it never gates\nvisibility. A SKILL-frontmatter file named `note-foo.md` still becomes\na skill.\n\nThe `codiedev push` response always says where the artifact landed \u2014\nread it back to the user so they can confirm.\n\n**Resolving \"the spec we just worked on\":**\n- If a file was recently edited in this session matching the conventions\n above, push that file.\n- If content was drafted inline in the conversation, write it to a file\n first (e.g., `/tmp/spec-xxx.md`) then push.\n\n**Teammate names:** first name usually works (`codiedev ping <name> ...`).\nIf ambiguous, the CLI returns candidates \u2014 retry with the full email.\n\n**Search before pushing.** For any \"save this as \u2026\" request, run\n`codiedev search \"<query>\"` first. If prior art exists, pull it and\niterate rather than duplicating.\n\n**Sharing vs sending:** `codiedev share` is silent (access only, no\nnotification). `codiedev send` both grants access AND pings the\nrecipient. Use send when the user is actively looping someone in.\n\n**Errors:**\n- \"not connected\" \u2192 user needs to run `npx codiedev connect` with their\n API token from https://codiedev.com/portal/integrations/claude-code\n- \"ambiguous recipient\" \u2192 surface candidates to the user\n";
147
+ /** The full Codiedev instructions (team-skill context + command table), for
148
+ * harnesses Codiedev launches itself (Codie, Codie's pi) rather than a
149
+ * user-owned instructions file. */
150
+ export declare function codiedevInstructionsText(): string;
146
151
  export declare function installClaudeCodeInstructions(): void;
147
152
  export declare function installCodexInstructions(): void;
148
153
  export declare function copilotInstructionsPath(): string;
package/dist/utils.js CHANGED
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DIRECT_UPLOAD_THRESHOLD_CHARS = exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS = exports.FINISH_ORPHAN_AGE_MS = exports.CLAIM_STALE_MS = exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = exports.PI_EXTENSION_SUBCOMMANDS = exports.CONFIG_PATH = exports.codiedevStateDir = exports.piExtensionPath = exports.inspectPiExtension = void 0;
36
+ exports.DIRECT_UPLOAD_THRESHOLD_CHARS = exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS = exports.FINISH_ORPHAN_AGE_MS = exports.CLAIM_STALE_MS = exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = exports.CODIEDEV_INSTRUCTIONS_BODY = exports.PI_EXTENSION_SUBCOMMANDS = exports.CONFIG_PATH = exports.codiedevStateDir = exports.piExtensionPath = exports.inspectPiExtension = void 0;
37
37
  exports.readConfig = readConfig;
38
38
  exports.writeConfig = writeConfig;
39
39
  exports.writeConfigAtPath = writeConfigAtPath;
@@ -78,6 +78,7 @@ exports.ensureAllCodiedevHooksInstalled = ensureAllCodiedevHooksInstalled;
78
78
  exports.hasDirectHookPath = hasDirectHookPath;
79
79
  exports.migrateHooksToLauncher = migrateHooksToLauncher;
80
80
  exports.ensureSessionStartHookInstalled = ensureSessionStartHookInstalled;
81
+ exports.codiedevInstructionsText = codiedevInstructionsText;
81
82
  exports.installClaudeCodeInstructions = installClaudeCodeInstructions;
82
83
  exports.installCodexInstructions = installCodexInstructions;
83
84
  exports.copilotInstructionsPath = copilotInstructionsPath;
@@ -1660,7 +1661,7 @@ const CODEX_USER_INSTRUCTIONS_PATH = path.join((0, harnessDirs_1.codexHome)(), "
1660
1661
  const CODEX_CONFIG_PATH = path.join((0, harnessDirs_1.codexHome)(), "config.toml");
1661
1662
  const CODIEDEV_INSTRUCTIONS_BEGIN = "<!-- codiedev-cli:begin -->";
1662
1663
  const CODIEDEV_INSTRUCTIONS_END = "<!-- codiedev-cli:end -->";
1663
- const CODIEDEV_INSTRUCTIONS_BODY = `## Codiedev — team artifact layer
1664
+ exports.CODIEDEV_INSTRUCTIONS_BODY = `## Codiedev — team artifact layer
1664
1665
 
1665
1666
  This machine is connected to Codiedev. Teammates share specs, reviews,
1666
1667
  decisions, and messages via a CLI on every dev's machine.
@@ -1689,8 +1690,41 @@ thought, use the \`codiedev\` CLI via Bash:**
1689
1690
  | "put this week's failed runs in the review queue" | \`codiedev traces queue list\`; \`codiedev traces queue add <queue> --agent <name> [--failed] [--days n]\` |
1690
1691
  | "have a judge score 10% of my agent's runs" / "turn off automatic scoring" | \`codiedev traces judge <agent> --on --scores <a,b> --sample 10 [--budget 25]\`; \`--off\`; no flags shows it |
1691
1692
  | "import these test cases from a CSV" / "add an example to the dataset" / "save the dataset as a version" | \`codiedev traces import <agent> <file> --dataset <name>\`; \`codiedev traces add-example <agent> --dataset <name> --input "…" [--expected "…"]\`; \`codiedev traces save-version <dataset>\`; \`codiedev traces versions <dataset>\` |
1693
+ | "make a starter pack" / "add this to our pack" / "package these skills for my team" | write a \`<slug>.pack.md\` (start from \`codiedev pack export <slug> --manifest --out .\` for an existing pack), then \`codiedev pack apply <file> [--publish]\` — see **Starter packs** below |
1694
+ | "publish the pack" / "ship the pack to my team" | \`codiedev pack publish <slug> [--channel stable\\|beta\\|alpha]\` |
1692
1695
  | "what was the last eval on X?" / "file these eval results" | \`codiedev evals last <subject>\` before starting a related eval; \`codiedev evals report <run.json>\` after each variant runs (scores only — \`codiedev evals help\` for the shape) |
1693
1696
 
1697
+ **Starter packs** (team managers and admins): a pack.md is frontmatter
1698
+ (\`name:\`, \`slug:\`, optional \`description:\`) then sections. Ask the user
1699
+ what to include, write the file, run \`codiedev pack apply <file> --dry-run\`
1700
+ to show them, then apply for real:
1701
+
1702
+ \`\`\`
1703
+ ---
1704
+ name: "Healthcare scan"
1705
+ slug: "healthcare-scan"
1706
+ ---
1707
+
1708
+ # Healthcare scan
1709
+
1710
+ ## Skills
1711
+
1712
+ - ./skills/healthcare-scan
1713
+ - lint.md @teammate@example.com
1714
+
1715
+ ## Files
1716
+
1717
+ - runner <- ~/.claude/runner
1718
+ \`\`\`
1719
+
1720
+ Under **Skills**, a local path (\`./…\`, \`~/…\` or \`/…\`) to a skill folder
1721
+ or SKILL.md is pushed to the library and added, with the rest of its folder
1722
+ shipped as the skill's files; \`<key> @<author email>\` adds a teammate's
1723
+ library skill. **Files** maps local folders to paths in the pack
1724
+ (\`.\` is the pack root). In skills, point at shipped files relative to the
1725
+ skill's folder or with \`\${CLAUDE_PLUGIN_ROOT}/…\`. Applying changes the
1726
+ draft; \`--publish\` (or \`codiedev pack publish\`) sends it to the team.
1727
+
1694
1728
  **Just push the file — we'll figure out where it goes.** Every artifact
1695
1729
  is classified into one of two kinds at upload time:
1696
1730
 
@@ -1731,13 +1765,19 @@ recipient. Use send when the user is actively looping someone in.
1731
1765
  API token from https://codiedev.com/portal/integrations/claude-code
1732
1766
  - "ambiguous recipient" → surface candidates to the user
1733
1767
  `;
1768
+ /** The full Codiedev instructions (team-skill context + command table), for
1769
+ * harnesses Codiedev launches itself (Codie, Codie's pi) rather than a
1770
+ * user-owned instructions file. */
1771
+ function codiedevInstructionsText() {
1772
+ return `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${exports.CODIEDEV_INSTRUCTIONS_BODY}\n`;
1773
+ }
1734
1774
  /**
1735
1775
  * Write the codiedev agent-facing docs block to the user-level Claude
1736
1776
  * instructions file so Claude Code picks up the commands in every session
1737
1777
  * (no per-repo CLAUDE.md needed). Idempotent — replaces the existing block
1738
1778
  * between its begin/end markers on re-run.
1739
1779
  */
1740
- function writeInstructionsBlock(targetPath, body = CODIEDEV_INSTRUCTIONS_BODY) {
1780
+ function writeInstructionsBlock(targetPath, body = exports.CODIEDEV_INSTRUCTIONS_BODY) {
1741
1781
  const dir = path.dirname(targetPath);
1742
1782
  if (!fs.existsSync(dir)) {
1743
1783
  fs.mkdirSync(dir, { recursive: true });
@@ -1784,10 +1824,10 @@ function piInstructionsPath() {
1784
1824
  /** pi has no session-start output channel, so team skill context rides in
1785
1825
  * AGENTS.md the way it does for Copilot. */
1786
1826
  function installPiInstructions() {
1787
- writeInstructionsBlock(piInstructionsPath(), `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}`);
1827
+ writeInstructionsBlock(piInstructionsPath(), `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${exports.CODIEDEV_INSTRUCTIONS_BODY}`);
1788
1828
  }
1789
1829
  function installCopilotInstructions() {
1790
- writeInstructionsBlock(copilotInstructionsPath(), `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}`);
1830
+ writeInstructionsBlock(copilotInstructionsPath(), `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${exports.CODIEDEV_INSTRUCTIONS_BODY}`);
1791
1831
  }
1792
1832
  // Cursor uses .mdc rule files with YAML frontmatter (description, alwaysApply,
1793
1833
  // globs). `.cursorrules` is deprecated and ignored by Agent mode as of 2026.
@@ -1804,7 +1844,7 @@ function installCursorInstructions() {
1804
1844
  const block = CURSOR_INSTRUCTIONS_FRONTMATTER +
1805
1845
  CODIEDEV_INSTRUCTIONS_BEGIN +
1806
1846
  "\n" +
1807
- `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}` +
1847
+ `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${exports.CODIEDEV_INSTRUCTIONS_BODY}` +
1808
1848
  "\n" +
1809
1849
  CODIEDEV_INSTRUCTIONS_END +
1810
1850
  "\n";
@@ -1822,7 +1862,7 @@ function installCursorInstructions() {
1822
1862
  existing.slice(0, beginIdx) +
1823
1863
  CODIEDEV_INSTRUCTIONS_BEGIN +
1824
1864
  "\n" +
1825
- `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}` +
1865
+ `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${exports.CODIEDEV_INSTRUCTIONS_BODY}` +
1826
1866
  "\n" +
1827
1867
  CODIEDEV_INSTRUCTIONS_END +
1828
1868
  existing.slice(endIdx + CODIEDEV_INSTRUCTIONS_END.length);
@@ -1833,10 +1873,10 @@ function installCursorInstructions() {
1833
1873
  fs.writeFileSync(CURSOR_RULES_PATH, next, "utf8");
1834
1874
  }
1835
1875
  function installedInstructionsTargets() {
1836
- const withTeamContext = `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${CODIEDEV_INSTRUCTIONS_BODY}`;
1876
+ const withTeamContext = `${teamSkillContext_1.TEAM_SKILL_CONTEXT}\n\n${exports.CODIEDEV_INSTRUCTIONS_BODY}`;
1837
1877
  return [
1838
- { path: CLAUDE_USER_INSTRUCTIONS_PATH, body: CODIEDEV_INSTRUCTIONS_BODY },
1839
- { path: CODEX_USER_INSTRUCTIONS_PATH, body: CODIEDEV_INSTRUCTIONS_BODY },
1878
+ { path: CLAUDE_USER_INSTRUCTIONS_PATH, body: exports.CODIEDEV_INSTRUCTIONS_BODY },
1879
+ { path: CODEX_USER_INSTRUCTIONS_PATH, body: exports.CODIEDEV_INSTRUCTIONS_BODY },
1840
1880
  { path: copilotInstructionsPath(), body: withTeamContext },
1841
1881
  { path: CURSOR_RULES_PATH, body: withTeamContext },
1842
1882
  { path: piInstructionsPath(), body: withTeamContext },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.86",
3
+ "version": "0.9.88",
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",