@c4a/context-cli 0.6.0-beta.2 → 0.6.0-beta.4

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/cli.js CHANGED
@@ -49728,8 +49728,15 @@ function extractPhaseSourceFingerprint(input) {
49728
49728
  ref: source3.record.git.ref,
49729
49729
  ...source3.status.head !== undefined ? { head: source3.status.head } : {},
49730
49730
  ...source3.record.subpath !== undefined ? { subpath: source3.record.subpath } : {},
49731
+ scopeHash: source3.status.scopeHash ?? "unknown",
49731
49732
  materializedAt: source3.status.materializedAt
49732
49733
  })).sort((left, right) => left.name.localeCompare(right.name));
49734
+ const freshnessSources = sources.map((source3) => ({
49735
+ name: source3.name,
49736
+ ...source3.subpath !== undefined ? { subpath: source3.subpath } : {},
49737
+ scopeHash: source3.scopeHash,
49738
+ materializedAt: source3.materializedAt
49739
+ }));
49733
49740
  const fingerprint = stableHash({
49734
49741
  phase: {
49735
49742
  id: input.phase.id,
@@ -49743,7 +49750,7 @@ function extractPhaseSourceFingerprint(input) {
49743
49750
  exportedOnly: input.phase.exportedOnly,
49744
49751
  transform: transformFingerprint(input.phase)
49745
49752
  },
49746
- sources
49753
+ sources: freshnessSources
49747
49754
  });
49748
49755
  return {
49749
49756
  phaseId: input.phase.id,
@@ -50510,12 +50517,19 @@ async function normalizeInputRef(input) {
50510
50517
  }
50511
50518
  return resolved.toLowerCase();
50512
50519
  }
50513
- function refMatchesHead(head, ref2, diagnostics) {
50520
+ async function gitTreeHash(input) {
50521
+ if (input.ref === undefined || !isFullCommitSha(input.ref))
50522
+ return;
50523
+ const treeish = input.subpath === undefined ? `${input.ref}^{tree}` : `${input.ref}:${input.subpath}`;
50524
+ const hash2 = await gitOutput(input.gitRoot, ["rev-parse", "--verify", treeish]);
50525
+ return hash2 !== null && /^[0-9a-f]{40}$/iu.test(hash2) ? hash2.toLowerCase() : undefined;
50526
+ }
50527
+ function validPinnedRef(ref2, diagnostics) {
50514
50528
  if (!isFullCommitSha(ref2)) {
50515
50529
  diagnostics.push(`registry ref must be a full 40-character commit sha: ${ref2}`);
50516
50530
  return false;
50517
50531
  }
50518
- return head === ref2;
50532
+ return true;
50519
50533
  }
50520
50534
  async function normalizeAddInput(input, existing) {
50521
50535
  const originalLocal = input.local ?? existing?.local;
@@ -50569,6 +50583,49 @@ async function normalizeAddInput(input, existing) {
50569
50583
  }
50570
50584
  };
50571
50585
  }
50586
+ async function inspectRepoCheckout(input) {
50587
+ if (input.localAbs === null) {
50588
+ input.diagnostics.push("local path is not declared");
50589
+ input.agentHints.push("Ask the user for a local checkout path before running extraction.");
50590
+ return { gitRepo: false };
50591
+ }
50592
+ if (!input.localExists) {
50593
+ input.diagnostics.push(`local path is missing: ${input.source.local}`);
50594
+ input.agentHints.push(`Ask the user before cloning ${input.source.git.remote} at ${input.source.git.ref}; the CLI will not clone automatically.`);
50595
+ return { gitRepo: false };
50596
+ }
50597
+ const gitRoot = await resolveGitRoot(input.localAbs);
50598
+ if (gitRoot === null) {
50599
+ input.diagnostics.push(`local path is not a git repository: ${input.source.local}`);
50600
+ return { gitRepo: false };
50601
+ }
50602
+ if (!input.scopeExists || input.scopedAbs === null) {
50603
+ input.diagnostics.push(`source subpath is missing: ${input.subpath ?? "."}`);
50604
+ return { gitRepo: true };
50605
+ }
50606
+ const head = await readGitHead(gitRoot) ?? await gitOutput(gitRoot, ["rev-parse", "HEAD"]) ?? undefined;
50607
+ if (!validPinnedRef(input.source.git.ref, input.diagnostics)) {
50608
+ return { gitRepo: true, ...head !== undefined ? { head } : {}, scopedAbs: input.scopedAbs };
50609
+ }
50610
+ const [pinnedScopeHash, scopeHash] = await Promise.all([
50611
+ gitTreeHash({ gitRoot, ref: input.source.git.ref, subpath: input.subpath }),
50612
+ gitTreeHash({ gitRoot, ref: head, subpath: input.subpath })
50613
+ ]);
50614
+ if (pinnedScopeHash === undefined) {
50615
+ input.diagnostics.push(`source boundary ${input.subpath ?? "."} is missing at pinned ref ${input.source.git.ref}`);
50616
+ input.agentHints.push("Ask the user to confirm a valid pinned source commit before updating the ref; the CLI will not change the source repository.");
50617
+ }
50618
+ if (scopeHash === undefined) {
50619
+ input.diagnostics.push(`source boundary ${input.subpath ?? "."} is missing at HEAD ${head ?? "<unknown>"}`);
50620
+ }
50621
+ return {
50622
+ gitRepo: true,
50623
+ ...head !== undefined ? { head } : {},
50624
+ ...pinnedScopeHash !== undefined ? { pinnedScopeHash } : {},
50625
+ ...scopeHash !== undefined ? { scopeHash } : {},
50626
+ scopedAbs: input.scopedAbs
50627
+ };
50628
+ }
50572
50629
  async function inspectRepoSource(input) {
50573
50630
  const source3 = input.source;
50574
50631
  const materializedAt = source3.materializedAt ?? defaultMaterializedAt(source3.name);
@@ -50579,43 +50636,36 @@ async function inspectRepoSource(input) {
50579
50636
  const subpath = normalizeSubpath(source3.subpath);
50580
50637
  const scopedAbs = localAbs === null ? null : scopedLocalPath(localAbs, subpath);
50581
50638
  const scopeExists = scopedAbs !== null && existsSync9(scopedAbs);
50582
- let gitRepo = false;
50583
- let head;
50584
50639
  let materialized = existsSync9(join13(input.projectRoot, materializedAt));
50585
- if (!localAbs) {
50586
- diagnostics.push("local path is not declared");
50587
- agent_hints.push("Ask the user for a local checkout path before running extraction.");
50588
- } else if (!localExists) {
50589
- diagnostics.push(`local path is missing: ${source3.local}`);
50590
- agent_hints.push(`Ask the user before cloning ${source3.git.remote} at ${source3.git.ref}; the CLI will not clone automatically.`);
50591
- } else {
50592
- const gitRoot = await resolveGitRoot(localAbs);
50593
- gitRepo = gitRoot !== null;
50594
- if (gitRoot === null) {
50595
- diagnostics.push(`local path is not a git repository: ${source3.local}`);
50596
- } else if (!scopeExists || scopedAbs === null) {
50597
- diagnostics.push(`source subpath is missing: ${subpath ?? "."}`);
50598
- } else {
50599
- head = await readGitHead(gitRoot) ?? await gitOutput(gitRoot, ["rev-parse", "HEAD"]) ?? undefined;
50600
- materialized = input.materialize ? await ensureMaterializedSymlink({
50601
- projectRoot: input.projectRoot,
50602
- materializedAt,
50603
- localAbs: scopedAbs,
50604
- diagnostics
50605
- }) : await diagnoseMaterializedSymlink({
50606
- projectRoot: input.projectRoot,
50607
- materializedAt,
50608
- localAbs: scopedAbs,
50609
- diagnostics,
50610
- agent_hints,
50611
- sourceName: source3.name
50612
- });
50613
- }
50640
+ const checkout = await inspectRepoCheckout({
50641
+ source: source3,
50642
+ localAbs,
50643
+ localExists,
50644
+ subpath,
50645
+ scopeExists,
50646
+ scopedAbs,
50647
+ diagnostics,
50648
+ agentHints: agent_hints
50649
+ });
50650
+ if (checkout.scopedAbs !== undefined) {
50651
+ materialized = input.materialize ? await ensureMaterializedSymlink({
50652
+ projectRoot: input.projectRoot,
50653
+ materializedAt,
50654
+ localAbs: checkout.scopedAbs,
50655
+ diagnostics
50656
+ }) : await diagnoseMaterializedSymlink({
50657
+ projectRoot: input.projectRoot,
50658
+ materializedAt,
50659
+ localAbs: checkout.scopedAbs,
50660
+ diagnostics,
50661
+ agent_hints,
50662
+ sourceName: source3.name
50663
+ });
50614
50664
  }
50615
- const refMatches = refMatchesHead(head, source3.git.ref, diagnostics);
50616
- if (gitRepo && !refMatches) {
50617
- diagnostics.push(`HEAD ${head ?? "<unknown>"} does not match pinned ref ${source3.git.ref}`);
50618
- agent_hints.push("Ask the user before running git checkout/reset; the CLI will not change the source repository.");
50665
+ const scopeMatches = checkout.pinnedScopeHash !== undefined && checkout.scopeHash !== undefined && checkout.pinnedScopeHash === checkout.scopeHash;
50666
+ if (checkout.gitRepo && checkout.pinnedScopeHash !== undefined && checkout.scopeHash !== undefined && !scopeMatches) {
50667
+ diagnostics.push(`source boundary ${subpath ?? "."} hash ${checkout.scopeHash} at HEAD ${checkout.head ?? "<unknown>"} does not match pinned ref ${source3.git.ref} hash ${checkout.pinnedScopeHash}`);
50668
+ agent_hints.push("Ask the user to confirm the changed source boundary before updating its pinned ref; the CLI will not change the source repository.");
50619
50669
  }
50620
50670
  return {
50621
50671
  name: source3.name,
@@ -50627,10 +50677,12 @@ async function inspectRepoSource(input) {
50627
50677
  materialized,
50628
50678
  localExists,
50629
50679
  ...subpath !== undefined ? { scopeExists } : {},
50630
- gitRepo,
50631
- ...head !== undefined ? { head } : {},
50632
- refMatches,
50633
- ready: localExists && scopeExists && gitRepo && refMatches && materialized && diagnostics.length === 0,
50680
+ gitRepo: checkout.gitRepo,
50681
+ ...checkout.head !== undefined ? { head: checkout.head } : {},
50682
+ ...checkout.pinnedScopeHash !== undefined ? { pinnedScopeHash: checkout.pinnedScopeHash } : {},
50683
+ ...checkout.scopeHash !== undefined ? { scopeHash: checkout.scopeHash } : {},
50684
+ scopeMatches,
50685
+ ready: localExists && scopeExists && checkout.gitRepo && scopeMatches && materialized && diagnostics.length === 0,
50634
50686
  diagnostics,
50635
50687
  agent_hints
50636
50688
  };
@@ -51216,6 +51268,7 @@ async function previewExtractTsPhase(input) {
51216
51268
  name: source3.record.name,
51217
51269
  ref: source3.record.git.ref,
51218
51270
  ...source3.status.head !== undefined ? { head: source3.status.head } : {},
51271
+ scopeHash: source3.status.scopeHash ?? "unknown",
51219
51272
  materializedAt: source3.status.materializedAt,
51220
51273
  modules,
51221
51274
  moduleErrors
@@ -56886,6 +56939,7 @@ function selectedSourcesForExtractPhase(input) {
56886
56939
  record: source3,
56887
56940
  status: {
56888
56941
  ...sourceStatus?.head !== undefined ? { head: sourceStatus.head } : {},
56942
+ ...sourceStatus?.scopeHash !== undefined ? { scopeHash: sourceStatus.scopeHash } : {},
56889
56943
  materializedAt: sourceStatus?.materializedAt ?? defaultMaterializedAt2(source3)
56890
56944
  }
56891
56945
  };
@@ -68758,6 +68812,9 @@ async function loadConfirmedStructure(input) {
68758
68812
  approvedStructureRestore: true
68759
68813
  });
68760
68814
  if (validated2.payload === undefined || !validated2.result.valid) {
68815
+ if ((input.allowInvalidStructureForReadOnly === true || input.allowInvalidApprovedStructureForReadOnly === true) && validated2.payload !== undefined) {
68816
+ return validated2.payload;
68817
+ }
68761
68818
  throw workspaceError2("knowledge/structure.yaml is not valid for compile", {
68762
68819
  path: APPROVED_STRUCTURE_FILE,
68763
68820
  diagnostics: validated2.result.diagnostics,
@@ -68847,7 +68904,8 @@ async function loadCompileInput(input) {
68847
68904
  projectRoot: input.projectRoot,
68848
68905
  phase: input.phase,
68849
68906
  evidence,
68850
- ...input.allowInvalidStructureForReadOnly === true ? { allowInvalidStructureForReadOnly: true } : {}
68907
+ ...input.allowInvalidStructureForReadOnly === true ? { allowInvalidStructureForReadOnly: true } : {},
68908
+ ...input.allowInvalidApprovedStructureForReadOnly === true ? { allowInvalidApprovedStructureForReadOnly: true } : {}
68851
68909
  });
68852
68910
  if (input.allowInvalidStructureForReadOnly === true) {
68853
68911
  return { evidence, structure };
@@ -69519,7 +69577,7 @@ async function runCompileProsePhase(input) {
69519
69577
  const view = input.options.schema === true ? "schema" : input.options.view ?? (input.options.validate || input.options.stage ? undefined : "read-plan");
69520
69578
  const { evidence, structure } = await loadCompileInput({
69521
69579
  ...input,
69522
- ...(view === "blockers" || view === "schema") && input.options.validate !== true && input.options.stage !== true ? { allowInvalidStructureForReadOnly: true } : {}
69580
+ ...(view === "blockers" || view === "schema") && input.options.validate !== true && input.options.stage !== true ? { allowInvalidStructureForReadOnly: true } : view !== undefined && input.options.validate !== true && input.options.stage !== true ? { allowInvalidApprovedStructureForReadOnly: true } : {}
69523
69581
  });
69524
69582
  if (input.options.validate !== true && input.options.stage !== true) {
69525
69583
  return await runCompileViewRequest({
@@ -72320,6 +72378,17 @@ async function runExternalOptional(command, args2) {
72320
72378
  async function commandAvailable(command) {
72321
72379
  return runExternalOptional("sh", ["-lc", `command -v ${command} >/dev/null 2>&1`]);
72322
72380
  }
72381
+ async function claudePluginInstalled() {
72382
+ try {
72383
+ const { stdout } = await execFileAsync5("claude", ["plugin", "list", "--json"], {
72384
+ maxBuffer: 1024 * 1024
72385
+ });
72386
+ const parsed = JSON.parse(stdout);
72387
+ return Array.isArray(parsed) && parsed.some((item) => item !== null && typeof item === "object" && ("id" in item) && item.id === PLUGIN_ID);
72388
+ } catch {
72389
+ return false;
72390
+ }
72391
+ }
72323
72392
  function missingAgentResult(agent) {
72324
72393
  return {
72325
72394
  agent,
@@ -72503,6 +72572,24 @@ async function pruneClaudeLegacyPluginCache(dryRun, steps) {
72503
72572
  });
72504
72573
  }
72505
72574
  }
72575
+ async function pruneClaudeSupersededContextCache(root2, dryRun, steps) {
72576
+ const manifest = await readFile31(join44(root2, "claude", ".claude-plugin", "plugin.json"), "utf8").then((content3) => JSON.parse(content3)).catch(() => null);
72577
+ const currentVersion = typeof manifest?.version === "string" ? manifest.version : null;
72578
+ if (!currentVersion)
72579
+ return;
72580
+ const pluginDir = join44(claudePluginCacheRoot(), MARKETPLACE_NAME, PLUGIN_NAME);
72581
+ const staleVersions = (await readdir15(pluginDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory() && entry.name !== currentVersion).map((entry) => entry.name);
72582
+ if (staleVersions.length === 0)
72583
+ return;
72584
+ steps.push({
72585
+ agent: "claude",
72586
+ command: `prune superseded Claude ${MARKETPLACE_NAME}/${PLUGIN_NAME} version(s): ${staleVersions.join(", ")}`,
72587
+ status: dryRun ? "planned" : "ran"
72588
+ });
72589
+ if (!dryRun) {
72590
+ await Promise.all(staleVersions.map((version3) => rm10(join44(pluginDir, version3), { recursive: true, force: true })));
72591
+ }
72592
+ }
72506
72593
  function enableCodexPluginConfig(content3) {
72507
72594
  const header = `[plugins."${PLUGIN_ID}"]`;
72508
72595
  const blockStart = content3.indexOf(header);
@@ -72604,14 +72691,22 @@ async function installClaude(root2, dryRun, steps) {
72604
72691
  const addArgs = ["plugin", "marketplace", "add", root2];
72605
72692
  const installArgs = ["plugin", "install", PLUGIN_ID, "--scope", "user"];
72606
72693
  steps.push({ agent: "claude", command: commandLine("claude", addArgs), status: dryRun ? "planned" : "ran" });
72607
- steps.push({ agent: "claude", command: commandLine("claude", installArgs), status: dryRun ? "planned" : "ran" });
72608
- if (dryRun)
72694
+ if (dryRun) {
72695
+ steps.push({ agent: "claude", command: commandLine("claude", installArgs), status: "planned" });
72609
72696
  return;
72697
+ }
72610
72698
  const added = await runExternalOptional("claude", addArgs);
72611
72699
  if (!added) {
72612
72700
  await runExternal("claude", ["plugin", "marketplace", "update", MARKETPLACE_NAME]);
72613
72701
  }
72702
+ if (await claudePluginInstalled()) {
72703
+ const uninstallArgs = ["plugin", "uninstall", PLUGIN_ID, "--scope", "user"];
72704
+ steps.push({ agent: "claude", command: commandLine("claude", uninstallArgs), status: "ran" });
72705
+ await runExternal("claude", uninstallArgs);
72706
+ }
72707
+ steps.push({ agent: "claude", command: commandLine("claude", installArgs), status: "ran" });
72614
72708
  await runExternal("claude", installArgs);
72709
+ await pruneClaudeSupersededContextCache(root2, dryRun, steps);
72615
72710
  }
72616
72711
  async function installCodex(root2, dryRun, steps) {
72617
72712
  await pruneLegacyCodexConfig(dryRun, steps);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@c4a/context-cli",
3
- "version": "0.6.0-beta.2",
3
+ "version": "0.6.0-beta.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "context": "./cli.js"
7
7
  },
8
8
  "dependencies": {
9
- "@c4a/context": "0.6.0-beta.2",
9
+ "@c4a/context": "0.6.0-beta.4",
10
10
  "commander": "^11.0.0",
11
11
  "handlebars": "^4.7.8",
12
12
  "ink": "^5.0.0",
package/plugins/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.0-beta.2
1
+ 0.6.0-beta.4
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "context",
3
3
  "description": "Maintain a project-local knowledge workspace through init and next-step agent guidance.",
4
- "version": "0.6.0-beta.2",
4
+ "version": "0.6.0-beta.4",
5
5
  "author": {
6
6
  "name": "c4a"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context",
3
- "version": "0.6.0-beta.2",
3
+ "version": "0.6.0-beta.4",
4
4
  "description": "Maintain a project-local knowledge workspace through init and next-step agent guidance.",
5
5
  "author": {
6
6
  "name": "c4a"
@@ -18,7 +18,7 @@
18
18
  "skills": "./skills/",
19
19
  "interface": {
20
20
  "displayName": "C4A Context",
21
- "shortDescription": "Initialize and advance a local, source-linked project knowledge workspace.\nv0.6.0-beta.2",
21
+ "shortDescription": "Initialize and advance a local, source-linked project knowledge workspace.\nv0.6.0-beta.4",
22
22
  "longDescription": "Create a Context workspace and use agent-guided next steps to register sources, run extraction, review candidates, build package outputs, and verify health without silently mutating source repositories.",
23
23
  "developerName": "c4a",
24
24
  "category": "Productivity",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "context",
3
3
  "displayName": "C4A Context",
4
- "version": "0.6.0-beta.2",
4
+ "version": "0.6.0-beta.4",
5
5
  "description": "Maintain a project-local knowledge workspace through init and next-step agent guidance.",
6
6
  "author": {
7
7
  "name": "Context4AI",