codiedev 0.9.49 → 0.9.51

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
@@ -53,6 +53,7 @@ const configDir_1 = require("./configDir");
53
53
  const codieLauncher_1 = require("./codieLauncher");
54
54
  const codieRuntime_1 = require("./codieRuntime");
55
55
  const codieRuntimeInstall_1 = require("./codieRuntimeInstall");
56
+ const codieStartup_1 = require("./codieStartup");
56
57
  const upgradeNudge_1 = require("./upgradeNudge");
57
58
  function git(args) {
58
59
  const result = (0, child_process_1.spawnSync)("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -300,6 +301,10 @@ async function main() {
300
301
  process.exitCode = update.status ?? 0;
301
302
  return;
302
303
  }
304
+ const policyRefresh = await (0, codieStartup_1.refreshCodieSecurityPolicy)(connected);
305
+ if (policyRefresh.status === "cached") {
306
+ console.error(`codie: security policy refresh failed; using the cached policy (${policyRefresh.error})`);
307
+ }
303
308
  const gatewayUrl = process.env.CODIEDEV_GATEWAY_URL || "https://gateway.codiedev.com/api/v1";
304
309
  const catalog = await (0, codieLauncher_1.fetchApprovedCatalog)({ baseUrl: gatewayUrl, key });
305
310
  const parsed = (0, codieLauncher_1.parseCodieArgs)(process.argv.slice(2));
@@ -310,11 +315,20 @@ async function main() {
310
315
  }
311
316
  (0, codieLauncher_1.validateCodieArgs)(parsed.args, catalog);
312
317
  const stateDir = (0, configDir_1.codiedevStateDir)();
313
- const runtime = (0, codieRuntimeInstall_1.ensureCodieRuntimeBinary)({
318
+ const runtime = await (0, codieRuntimeInstall_1.ensureCodieRuntimeBinary)({
314
319
  resolve: () => (0, codieRuntime_1.resolveCodieRuntime)({
315
320
  env: process.env,
316
321
  stateDir,
317
322
  existsSync: (candidate) => fs.existsSync(candidate),
323
+ readInstalledVersion: (candidate) => {
324
+ try {
325
+ const value = JSON.parse(fs.readFileSync(candidate, "utf8"));
326
+ return typeof value.version === "string" ? value.version : null;
327
+ }
328
+ catch {
329
+ return null;
330
+ }
331
+ },
318
332
  resolveBundled: () => {
319
333
  try {
320
334
  return require.resolve("opencode-ai/bin/opencode.exe");
@@ -325,6 +339,16 @@ async function main() {
325
339
  },
326
340
  }),
327
341
  install: (missing) => (0, codieRuntimeInstall_1.installCodieRuntimeOnDisk)(missing, stateDir),
342
+ confirmUpdate: async (outdated) => {
343
+ console.error("");
344
+ console.error(`Codie harness ${outdated.expectedVersion} is available (you're on ${outdated.currentVersion}).`);
345
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
346
+ console.error("Restart `codie` in an interactive terminal to update the harness.");
347
+ console.error("");
348
+ return false;
349
+ }
350
+ return await (0, upgradeNudge_1.promptYesNo)(" Update the harness now? [y/N] ");
351
+ },
328
352
  });
329
353
  const metadataPlugin = (0, url_1.pathToFileURL)(assetPath("opencode-plugin.mjs")).href;
330
354
  const capturePlugin = (0, codieLauncher_1.resolveCapturePlugin)({ homeDir: os.homedir(), existsSync: fs.existsSync });
@@ -1,4 +1,5 @@
1
1
  export declare const OPENCODE_RUNTIME_SPEC = "opencode-ai@1.18.4";
2
+ export declare const OPENCODE_RUNTIME_VERSION: string;
2
3
  export declare const CODIE_MIN_NODE_MAJOR = 20;
3
4
  export type CodieRuntime = {
4
5
  kind: "ready";
@@ -8,12 +9,21 @@ export type CodieRuntime = {
8
9
  installDir: string;
9
10
  binary: string;
10
11
  spec: string;
12
+ } | {
13
+ kind: "outdated";
14
+ installDir: string;
15
+ binary: string;
16
+ spec: string;
17
+ currentVersion: string;
18
+ expectedVersion: string;
11
19
  };
12
20
  export declare function codieRuntimeDir(stateDir: string): string;
13
- export declare function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, }: {
21
+ export declare function installedRuntimePackageJson(stateDir: string): string;
22
+ export declare function resolveCodieRuntime({ env, stateDir, existsSync, readInstalledVersion, resolveBundled, }: {
14
23
  env: Record<string, string | undefined>;
15
24
  stateDir: string;
16
25
  existsSync: (candidate: string) => boolean;
26
+ readInstalledVersion?: (packageJson: string) => string | null;
17
27
  resolveBundled: () => string | null;
18
28
  }): CodieRuntime;
19
29
  export declare function assertCodieNodeVersion(version: string): void;
@@ -43,8 +43,9 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  };
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
- exports.CODIE_MIN_NODE_MAJOR = exports.OPENCODE_RUNTIME_SPEC = void 0;
46
+ exports.CODIE_MIN_NODE_MAJOR = exports.OPENCODE_RUNTIME_VERSION = exports.OPENCODE_RUNTIME_SPEC = void 0;
47
47
  exports.codieRuntimeDir = codieRuntimeDir;
48
+ exports.installedRuntimePackageJson = installedRuntimePackageJson;
48
49
  exports.resolveCodieRuntime = resolveCodieRuntime;
49
50
  exports.assertCodieNodeVersion = assertCodieNodeVersion;
50
51
  const path = __importStar(require("path"));
@@ -52,6 +53,7 @@ const path = __importStar(require("path"));
52
53
  // recognizes byte-for-byte, so a floating version would silently regress the
53
54
  // terminal branding whenever OpenCode ships a new build.
54
55
  exports.OPENCODE_RUNTIME_SPEC = "opencode-ai@1.18.4";
56
+ exports.OPENCODE_RUNTIME_VERSION = exports.OPENCODE_RUNTIME_SPEC.slice(exports.OPENCODE_RUNTIME_SPEC.lastIndexOf("@") + 1);
55
57
  // The harness needs 20; capture does not. package.json `engines` stays >=18 so
56
58
  // `codiedev connect` still installs for Node 18 users — this floor is enforced
57
59
  // at `codie` launch instead.
@@ -62,7 +64,10 @@ function codieRuntimeDir(stateDir) {
62
64
  function installedBinary(stateDir) {
63
65
  return path.join(codieRuntimeDir(stateDir), "node_modules", "opencode-ai", "bin", "opencode.exe");
64
66
  }
65
- function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, }) {
67
+ function installedRuntimePackageJson(stateDir) {
68
+ return path.join(codieRuntimeDir(stateDir), "node_modules", "opencode-ai", "package.json");
69
+ }
70
+ function resolveCodieRuntime({ env, stateDir, existsSync, readInstalledVersion, resolveBundled, }) {
66
71
  // 1. Explicit override — the air-gapped / pre-provisioned escape hatch.
67
72
  const override = env.CODIEDEV_OPENCODE_BINARY?.trim();
68
73
  if (override) {
@@ -78,8 +83,20 @@ function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, }) {
78
83
  return { kind: "ready", binary: bundled };
79
84
  // 3. The lazily installed engine.
80
85
  const binary = installedBinary(stateDir);
81
- if (existsSync(binary))
86
+ if (existsSync(binary)) {
87
+ const currentVersion = readInstalledVersion?.(installedRuntimePackageJson(stateDir)) ?? null;
88
+ if (currentVersion && currentVersion !== exports.OPENCODE_RUNTIME_VERSION) {
89
+ return {
90
+ kind: "outdated",
91
+ installDir: codieRuntimeDir(stateDir),
92
+ binary,
93
+ spec: exports.OPENCODE_RUNTIME_SPEC,
94
+ currentVersion,
95
+ expectedVersion: exports.OPENCODE_RUNTIME_VERSION,
96
+ };
97
+ }
82
98
  return { kind: "ready", binary };
99
+ }
83
100
  return { kind: "missing", installDir: codieRuntimeDir(stateDir), binary, spec: exports.OPENCODE_RUNTIME_SPEC };
84
101
  }
85
102
  function assertCodieNodeVersion(version) {
@@ -1,9 +1,10 @@
1
1
  import { CodieRuntime } from "./codieRuntime";
2
+ type InstallableCodieRuntime = Exclude<CodieRuntime, {
3
+ kind: "ready";
4
+ }>;
2
5
  export declare function buildRuntimeInstallArgs(stagingDir: string, spec: string): string[];
3
6
  export interface InstallDeps {
4
- runtime: Extract<CodieRuntime, {
5
- kind: "missing";
6
- }>;
7
+ runtime: InstallableCodieRuntime;
7
8
  makeStagingDir: () => string;
8
9
  run: (args: string[]) => {
9
10
  status: number | null;
@@ -15,13 +16,13 @@ export interface InstallDeps {
15
16
  log: (message: string) => void;
16
17
  }
17
18
  export declare function installCodieRuntime(deps: InstallDeps): void;
18
- export declare function ensureCodieRuntimeBinary({ resolve, install, }: {
19
+ export declare function ensureCodieRuntimeBinary({ resolve, install, confirmUpdate, }: {
19
20
  resolve: () => CodieRuntime;
20
- install: (runtime: Extract<CodieRuntime, {
21
- kind: "missing";
22
- }>) => void;
23
- }): string;
24
- export declare function installCodieRuntimeOnDisk(runtime: Extract<CodieRuntime, {
25
- kind: "missing";
26
- }>, stateDir: string): void;
21
+ install: (runtime: InstallableCodieRuntime) => void;
22
+ confirmUpdate?: (runtime: Extract<CodieRuntime, {
23
+ kind: "outdated";
24
+ }>) => Promise<boolean>;
25
+ }): Promise<string>;
26
+ export declare function installCodieRuntimeOnDisk(runtime: InstallableCodieRuntime, stateDir: string): void;
27
27
  export declare function defaultStagingRoot(): string;
28
+ export {};
@@ -98,10 +98,12 @@ function installCodieRuntime(deps) {
98
98
  }
99
99
  }
100
100
  // Resolve the engine, downloading it once if this is the first `codie` run.
101
- function ensureCodieRuntimeBinary({ resolve, install, }) {
101
+ async function ensureCodieRuntimeBinary({ resolve, install, confirmUpdate = async () => false, }) {
102
102
  const runtime = resolve();
103
103
  if (runtime.kind === "ready")
104
104
  return runtime.binary;
105
+ if (runtime.kind === "outdated" && !(await confirmUpdate(runtime)))
106
+ return runtime.binary;
105
107
  install(runtime);
106
108
  return runtime.binary;
107
109
  }
@@ -0,0 +1,24 @@
1
+ import type { AgentSurface, EndpointSecurityPolicy } from "./security";
2
+ import type { AgentSecurityEntitlement } from "./securityFeature";
3
+ export interface CodieStartupConfig extends AgentSecurityEntitlement {
4
+ backendUrl?: string;
5
+ token?: string;
6
+ companyId?: string;
7
+ }
8
+ export type CodiePolicyRefreshResult = {
9
+ status: "disabled";
10
+ } | {
11
+ status: "synced";
12
+ policy: EndpointSecurityPolicy;
13
+ } | {
14
+ status: "cached";
15
+ error: string;
16
+ };
17
+ /** Refresh the endpoint policy before Codie hands the terminal to OpenCode.
18
+ * Enforcement remains available offline because hooks keep using the last
19
+ * verified company-scoped policy when this best-effort refresh cannot land. */
20
+ export declare function refreshCodieSecurityPolicy(config: CodieStartupConfig, syncPolicy?: (config: {
21
+ backendUrl: string;
22
+ token: string;
23
+ companyId: string;
24
+ }, source?: AgentSurface) => Promise<EndpointSecurityPolicy>): Promise<CodiePolicyRefreshResult>;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.refreshCodieSecurityPolicy = refreshCodieSecurityPolicy;
4
+ const securityFeature_1 = require("./securityFeature");
5
+ const securityPolicy_1 = require("./securityPolicy");
6
+ /** Refresh the endpoint policy before Codie hands the terminal to OpenCode.
7
+ * Enforcement remains available offline because hooks keep using the last
8
+ * verified company-scoped policy when this best-effort refresh cannot land. */
9
+ async function refreshCodieSecurityPolicy(config, syncPolicy = securityPolicy_1.syncEndpointSecurityPolicy) {
10
+ if (!(0, securityFeature_1.isAgentSecurityEnabled)(config))
11
+ return { status: "disabled" };
12
+ if (!config.backendUrl || !config.token || !config.companyId) {
13
+ return { status: "cached", error: "connected account details are incomplete" };
14
+ }
15
+ try {
16
+ const policy = await syncPolicy({
17
+ backendUrl: config.backendUrl,
18
+ token: config.token,
19
+ companyId: config.companyId,
20
+ }, "codie-harness");
21
+ return { status: "synced", policy };
22
+ }
23
+ catch (error) {
24
+ return {
25
+ status: "cached",
26
+ error: error instanceof Error ? error.message : String(error),
27
+ };
28
+ }
29
+ }
package/dist/hook.js CHANGED
@@ -366,7 +366,11 @@ async function runSecurityEventUpload() {
366
366
  endpointName: config.deviceName,
367
367
  cliVersion: version_1.CLI_VERSION,
368
368
  });
369
- const result = await postJsonForResponse(`${config.backendUrl}/api/cli/securityEvent`, { ...eventWithEndpoint }, config.token, 1500);
369
+ const result = await postJsonForResponse(`${config.backendUrl}/api/cli/securityEvent`, { ...eventWithEndpoint }, config.token,
370
+ // Leave room for a cold backend and durable scheduling. This uploader is
371
+ // detached, so the network budget never delays the developer's prompt or
372
+ // tool call. Policy refreshes use the separately signed sync endpoint.
373
+ 5_000);
370
374
  if (result?.policy?.schemaVersion === 1) {
371
375
  (0, securityPolicy_1.writeEndpointSecurityPolicy)(result.policy, undefined, undefined, config.companyId);
372
376
  }
@@ -13,14 +13,20 @@ function parseRemoteUrl(url) {
13
13
  let s = url.trim();
14
14
  if (s.endsWith(".git"))
15
15
  s = s.slice(0, -4);
16
- const ssh = s.match(/^git@([^:]+):(.+)$/);
17
- if (ssh) {
18
- return { host: ssh[1].toLowerCase(), ownerName: ssh[2].toLowerCase() };
19
- }
20
16
  const https = s.match(/^https?:\/\/([^/]+)\/(.+)$/);
21
17
  if (https) {
22
18
  return { host: https[1].toLowerCase(), ownerName: https[2].toLowerCase() };
23
19
  }
20
+ // Codie strips SSH userinfo before putting a remote in the gateway header,
21
+ // so `git@gitlab.com:owner/repo.git` arrives here as
22
+ // `gitlab.com:owner/repo.git`. Accept both forms; otherwise every SSH-cloned
23
+ // Harness checkout loses its repoId even though transcript capture can
24
+ // resolve the same raw remote correctly. Check this after URL forms so the
25
+ // colon in `https://` is never mistaken for an SCP separator.
26
+ const scp = s.match(/^(?:[^@\s]+@)?([^:/\s]+):(.+)$/);
27
+ if (scp) {
28
+ return { host: scp[1].toLowerCase(), ownerName: scp[2].toLowerCase() };
29
+ }
24
30
  return null;
25
31
  }
26
32
  function resolveRepoForCapture(input) {
@@ -1,5 +1,13 @@
1
1
  import type { AgentSurface, EndpointSecurityPolicy } from "./security";
2
2
  export type TrustedPolicyKeys = Record<string, string>;
3
+ /**
4
+ * Vendor policy roots are compiled into the endpoint binary. This prevents a
5
+ * compromised control-plane response from introducing the key that verifies
6
+ * that same response. Additional customer-managed roots may still be supplied
7
+ * through CODIEDEV_SECURITY_POLICY_PUBLIC_KEYS, but they cannot replace the
8
+ * Codiedev production root with the same key id.
9
+ */
10
+ export declare const BUILT_IN_POLICY_KEYS: TrustedPolicyKeys;
3
11
  export declare function trustedPolicyKeysFromEnvironment(env?: NodeJS.ProcessEnv): TrustedPolicyKeys;
4
12
  export declare function verifyEndpointSecurityPolicySignature(policy: EndpointSecurityPolicy, trustedKeys?: TrustedPolicyKeys): {
5
13
  ok: boolean;
@@ -21,4 +29,4 @@ export declare function syncEndpointSecurityPolicy(config: {
21
29
  backendUrl: string;
22
30
  token: string;
23
31
  companyId: string;
24
- }, source?: AgentSurface, timeoutMs?: number): Promise<EndpointSecurityPolicy>;
32
+ }, source?: AgentSurface, timeoutMs?: number, stateDir?: string, trustedKeys?: TrustedPolicyKeys): Promise<EndpointSecurityPolicy>;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BUILT_IN_POLICY_KEYS = void 0;
36
37
  exports.trustedPolicyKeysFromEnvironment = trustedPolicyKeysFromEnvironment;
37
38
  exports.verifyEndpointSecurityPolicySignature = verifyEndpointSecurityPolicySignature;
38
39
  exports.readEndpointSecurityPolicy = readEndpointSecurityPolicy;
@@ -52,6 +53,16 @@ function policyFile(companyId) {
52
53
  const scope = companyId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128);
53
54
  return `security-policy.${scope}.json`;
54
55
  }
56
+ /**
57
+ * Vendor policy roots are compiled into the endpoint binary. This prevents a
58
+ * compromised control-plane response from introducing the key that verifies
59
+ * that same response. Additional customer-managed roots may still be supplied
60
+ * through CODIEDEV_SECURITY_POLICY_PUBLIC_KEYS, but they cannot replace the
61
+ * Codiedev production root with the same key id.
62
+ */
63
+ exports.BUILT_IN_POLICY_KEYS = {
64
+ "codiedev-prod-2026-01": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA0H+UISjTn4zhd6Pk9YmrMBqnJEJmv9fXoavXorlV2cU=\n-----END PUBLIC KEY-----\n",
65
+ };
55
66
  function stableJson(value) {
56
67
  if (Array.isArray(value))
57
68
  return `[${value.map(stableJson).join(",")}]`;
@@ -66,16 +77,17 @@ function stableJson(value) {
66
77
  function trustedPolicyKeysFromEnvironment(env = process.env) {
67
78
  const raw = env.CODIEDEV_SECURITY_POLICY_PUBLIC_KEYS;
68
79
  if (!raw)
69
- return {};
80
+ return { ...exports.BUILT_IN_POLICY_KEYS };
70
81
  try {
71
82
  const parsed = JSON.parse(raw);
72
83
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
73
- return {};
74
- return Object.fromEntries(Object.entries(parsed)
84
+ return { ...exports.BUILT_IN_POLICY_KEYS };
85
+ const configured = Object.fromEntries(Object.entries(parsed)
75
86
  .filter((entry) => typeof entry[1] === "string"));
87
+ return { ...configured, ...exports.BUILT_IN_POLICY_KEYS };
76
88
  }
77
89
  catch {
78
- return {};
90
+ return { ...exports.BUILT_IN_POLICY_KEYS };
79
91
  }
80
92
  }
81
93
  function verifyEndpointSecurityPolicySignature(policy, trustedKeys = trustedPolicyKeysFromEnvironment()) {
@@ -134,7 +146,7 @@ function writeEndpointSecurityPolicy(policy, stateDir = (0, configDir_1.codiedev
134
146
  fs.chmodSync(temporary, 0o600);
135
147
  fs.renameSync(temporary, target);
136
148
  }
137
- async function syncEndpointSecurityPolicy(config, source, timeoutMs = 3_000) {
149
+ async function syncEndpointSecurityPolicy(config, source, timeoutMs = 3_000, stateDir = (0, configDir_1.codiedevStateDir)(), trustedKeys = trustedPolicyKeysFromEnvironment()) {
138
150
  const url = new URL("/api/cli/securityPolicy", config.backendUrl);
139
151
  if (source)
140
152
  url.searchParams.set("source", source);
@@ -162,6 +174,6 @@ async function syncEndpointSecurityPolicy(config, source, timeoutMs = 3_000) {
162
174
  request.on("error", reject);
163
175
  request.end();
164
176
  });
165
- writeEndpointSecurityPolicy(policy, undefined, undefined, config.companyId);
177
+ writeEndpointSecurityPolicy(policy, stateDir, trustedKeys, config.companyId);
166
178
  return policy;
167
179
  }
@@ -13,6 +13,7 @@ export interface OfferOpts {
13
13
  log?: (line: string) => void;
14
14
  isTty?: boolean;
15
15
  }
16
+ export declare function promptYesNo(question: string): Promise<boolean>;
16
17
  /**
17
18
  * Prompt-capable sibling of maybePrintUpgradeNudge. Call this before handing
18
19
  * the terminal to a TUI. A successful upgrade relaunches the fresh CLI and
@@ -43,6 +43,7 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  })();
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.maybePrintUpgradeNudge = maybePrintUpgradeNudge;
46
+ exports.promptYesNo = promptYesNo;
46
47
  exports.maybeOfferUpgrade = maybeOfferUpgrade;
47
48
  const fs = __importStar(require("fs"));
48
49
  const path = __importStar(require("path"));
package/dist/utils.js CHANGED
@@ -2787,6 +2787,14 @@ async function runCaptureUpload(jobFile, deps) {
2787
2787
  const { storageId } = await deps.uploadBytes(uploadUrl, Buffer.from(transcript, "base64"), "application/json");
2788
2788
  body = { ...body, transcriptStorageId: storageId };
2789
2789
  delete body.transcript;
2790
+ // Persist the storage id before the metadata POST. Previously a failed
2791
+ // captureSession request retried the entire direct upload, creating one
2792
+ // unreferenced raw transcript blob per attempt. Once the PUT succeeds,
2793
+ // every retry must reuse that exact blob.
2794
+ job.body = body;
2795
+ if (deps.writeFileSync) {
2796
+ deps.writeFileSync(jobFile, JSON.stringify(job), { mode: 0o600 });
2797
+ }
2790
2798
  }
2791
2799
  await deps.post(job.url, body, job.token);
2792
2800
  // success — drop a "last session captured" breadcrumb (before removing the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.49",
3
+ "version": "0.9.51",
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",
@@ -14,6 +14,7 @@
14
14
  "test": "vitest run",
15
15
  "test:security-beta": "vitest run src/__tests__/security.test.ts src/__tests__/securityCommandEntitlement.test.ts src/__tests__/securityDeepScan.test.ts src/__tests__/securityDoctor.test.ts src/__tests__/securityEntitlementInstall.test.ts src/__tests__/securityFeature.test.ts src/__tests__/securityInstall.test.ts src/__tests__/securityInventory.test.ts src/__tests__/securityPilot.test.ts src/__tests__/securityPolicy.test.ts src/__tests__/securitySession.test.ts src/__tests__/codexHookTrust.test.ts src/__tests__/codieSecurityAdapterWiring.test.ts && tsc && node dist/cli.js security pilot --json",
16
16
  "test:security-pilot": "vitest run src/__tests__/security.test.ts src/__tests__/securityInstall.test.ts src/__tests__/securityPilot.test.ts src/__tests__/codieSecurityAdapterWiring.test.ts && tsc && node dist/cli.js security pilot --json",
17
+ "test:opencode-attribution": "vitest run src/__tests__/repoResolver.test.ts src/__tests__/openCodeCapture.test.ts src/__tests__/openCodePluginGitRefs.test.ts src/__tests__/capturePayloadFinal.test.ts src/__tests__/captureDirectUpload.test.ts src/__tests__/resolveCaptureFinal.test.ts && tsc",
17
18
  "test:watch": "vitest"
18
19
  },
19
20
  "files": [