threadnote 0.6.2 → 0.6.3

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.
Files changed (2) hide show
  1. package/dist/threadnote.cjs +82 -11
  2. package/package.json +1 -1
@@ -3488,6 +3488,7 @@ var SHIM_MARKER = "Generated by threadnote";
3488
3488
  var USER_INSTRUCTIONS_START_MARKER = "<!-- BEGIN THREADNOTE USER INSTRUCTIONS -->";
3489
3489
  var USER_INSTRUCTIONS_END_MARKER = "<!-- END THREADNOTE USER INSTRUCTIONS -->";
3490
3490
  var USER_MANIFEST_NAME = "seed-manifest.yaml";
3491
+ var SEED_STATE_FILE = "seed-state.json";
3491
3492
  var USER_AGENT_INSTRUCTION_TARGETS = [
3492
3493
  { kind: "block", label: "codex user instructions", path: "~/.codex/AGENTS.md" },
3493
3494
  { kind: "block", label: "claude user instructions", path: "~/.claude/CLAUDE.md" },
@@ -4256,8 +4257,11 @@ function buildCopilotMcpServerConfig(config, options) {
4256
4257
  type: "stdio"
4257
4258
  };
4258
4259
  }
4260
+ function isEmptyConfigContent(content) {
4261
+ return content === void 0 || content.trim().length === 0;
4262
+ }
4259
4263
  function renderCursorMcpConfig(currentContent, name, serverConfig) {
4260
- const parsed = currentContent === void 0 ? {} : parseJsonConfigObject(currentContent);
4264
+ const parsed = isEmptyConfigContent(currentContent) ? {} : parseJsonConfigObject(currentContent ?? "");
4261
4265
  if (parsed === void 0) {
4262
4266
  throw new Error(`${cursorMcpConfigPath()} exists but is not a JSON object; not modifying it.`);
4263
4267
  }
@@ -4272,7 +4276,7 @@ function renderCursorMcpConfig(currentContent, name, serverConfig) {
4272
4276
  `;
4273
4277
  }
4274
4278
  function renderCopilotMcpConfig(currentContent, name, serverConfig) {
4275
- const parsed = currentContent === void 0 ? {} : parseJsonConfigObject(currentContent);
4279
+ const parsed = isEmptyConfigContent(currentContent) ? {} : parseJsonConfigObject(currentContent ?? "");
4276
4280
  if (parsed === void 0) {
4277
4281
  throw new Error(`${copilotMcpConfigPath()} exists but is not a JSON object; not modifying it.`);
4278
4282
  }
@@ -4289,11 +4293,11 @@ function renderCopilotMcpConfig(currentContent, name, serverConfig) {
4289
4293
  async function removeCursorMcpConfig(name, dryRun) {
4290
4294
  const path = cursorMcpConfigPath();
4291
4295
  const currentContent = await readFileIfExists(path);
4292
- if (currentContent === void 0) {
4296
+ if (isEmptyConfigContent(currentContent)) {
4293
4297
  console.log(`Already absent: ${path}`);
4294
4298
  return;
4295
4299
  }
4296
- const parsed = parseJsonConfigObject(currentContent);
4300
+ const parsed = parseJsonConfigObject(currentContent ?? "");
4297
4301
  if (parsed === void 0) {
4298
4302
  console.log(`WARN ${path} exists but is not a JSON object; not modifying it.`);
4299
4303
  return;
@@ -4318,11 +4322,11 @@ async function removeCursorMcpConfig(name, dryRun) {
4318
4322
  async function removeCopilotMcpConfig(name, dryRun) {
4319
4323
  const path = copilotMcpConfigPath();
4320
4324
  const currentContent = await readFileIfExists(path);
4321
- if (currentContent === void 0) {
4325
+ if (isEmptyConfigContent(currentContent)) {
4322
4326
  console.log(`Already absent: ${path}`);
4323
4327
  return;
4324
4328
  }
4325
- const parsed = parseJsonConfigObject(currentContent);
4329
+ const parsed = parseJsonConfigObject(currentContent ?? "");
4326
4330
  if (parsed === void 0) {
4327
4331
  console.log(`WARN ${path} exists but is not a JSON object; not modifying it.`);
4328
4332
  return;
@@ -7698,8 +7702,8 @@ async function removeVikingResourceWithRetry(ov, config, uri) {
7698
7702
  return false;
7699
7703
  }
7700
7704
  async function vikingResourceExists(ov, config, uri) {
7701
- const stat3 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
7702
- return stat3.exitCode === 0;
7705
+ const stat4 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
7706
+ return stat4.exitCode === 0;
7703
7707
  }
7704
7708
  async function ensureDurableMemoryDirectory(ov, config) {
7705
7709
  await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
@@ -8481,9 +8485,13 @@ async function runSeed(config, options) {
8481
8485
  const manifest = await readSeedManifest(config.manifestPath);
8482
8486
  const ignorePatterns = await loadIgnorePatterns();
8483
8487
  const ov = await openVikingCliForMode(options.dryRun === true);
8488
+ const statePath = (0, import_node_path8.join)(config.agentContextHome, SEED_STATE_FILE);
8489
+ const state = options.force === true ? { files: {}, version: 1 } : await readSeedState(statePath);
8490
+ const projects = filterProjects(manifest.projects, options.only);
8484
8491
  let importedCount = 0;
8485
8492
  let skippedCount = 0;
8486
- for (const project of manifest.projects) {
8493
+ let unchangedCount = 0;
8494
+ for (const project of projects) {
8487
8495
  const projectRoot = expandPath(project.path);
8488
8496
  if (!await exists(projectRoot)) {
8489
8497
  console.log(`WARN project missing: ${project.name} (${projectRoot})`);
@@ -8491,6 +8499,12 @@ async function runSeed(config, options) {
8491
8499
  }
8492
8500
  const candidates = await collectSeedCandidates(project, projectRoot, ignorePatterns);
8493
8501
  for (const candidate of candidates) {
8502
+ const fileStat = await statSeedFile(candidate.filePath);
8503
+ const recorded = state.files[candidate.destinationUri];
8504
+ if (fileStat && recorded && recorded.mtimeMs === fileStat.mtimeMs && recorded.size === fileStat.size) {
8505
+ unchangedCount += 1;
8506
+ continue;
8507
+ }
8494
8508
  const importPath = await prepareSeedFile(config, candidate, options.dryRun === true);
8495
8509
  if (!importPath) {
8496
8510
  skippedCount += 1;
@@ -8499,9 +8513,61 @@ async function runSeed(config, options) {
8499
8513
  const args = withIdentity(config, ["add-resource", importPath, "--to", candidate.destinationUri, "--wait"]);
8500
8514
  await maybeRun(options.dryRun === true, ov, args);
8501
8515
  importedCount += 1;
8516
+ if (fileStat && options.dryRun !== true) {
8517
+ state.files[candidate.destinationUri] = { mtimeMs: fileStat.mtimeMs, size: fileStat.size };
8518
+ }
8519
+ }
8520
+ }
8521
+ if (options.dryRun !== true) {
8522
+ await writeSeedState(statePath, state);
8523
+ }
8524
+ console.log(
8525
+ `Seed complete: ${importedCount} candidate(s), ${unchangedCount} unchanged, ${skippedCount} skipped for safety.`
8526
+ );
8527
+ }
8528
+ function filterProjects(projects, only) {
8529
+ if (!only || only.length === 0) {
8530
+ return projects;
8531
+ }
8532
+ const known = new Set(projects.map((project) => project.name));
8533
+ const missing = only.filter((name) => !known.has(name));
8534
+ if (missing.length > 0) {
8535
+ const all = [...known].join(", ");
8536
+ throw new Error(`Unknown project(s) in --only: ${missing.join(", ")}. Manifest projects: ${all}`);
8537
+ }
8538
+ const want = new Set(only);
8539
+ return projects.filter((project) => want.has(project.name));
8540
+ }
8541
+ async function statSeedFile(path) {
8542
+ try {
8543
+ const result = await (0, import_promises7.stat)(path);
8544
+ return { mtimeMs: result.mtimeMs, size: result.size };
8545
+ } catch (_err) {
8546
+ return void 0;
8547
+ }
8548
+ }
8549
+ async function readSeedState(path) {
8550
+ try {
8551
+ const raw = await (0, import_promises7.readFile)(path, "utf8");
8552
+ const parsed = JSON.parse(raw);
8553
+ if (parsed.version !== 1 || !isJsonObject(parsed.files)) {
8554
+ return { files: {}, version: 1 };
8555
+ }
8556
+ const files = {};
8557
+ for (const [uri, entry] of Object.entries(parsed.files)) {
8558
+ if (isJsonObject(entry) && typeof entry.mtimeMs === "number" && typeof entry.size === "number") {
8559
+ files[uri] = { mtimeMs: entry.mtimeMs, size: entry.size };
8560
+ }
8502
8561
  }
8562
+ return { files, version: 1 };
8563
+ } catch (_err) {
8564
+ return { files: {}, version: 1 };
8503
8565
  }
8504
- console.log(`Seed complete: ${importedCount} candidate(s), ${skippedCount} skipped for safety.`);
8566
+ }
8567
+ async function writeSeedState(path, state) {
8568
+ await ensureDirectory((0, import_node_path8.dirname)(path), false);
8569
+ await (0, import_promises7.writeFile)(path, `${JSON.stringify(state, void 0, 2)}
8570
+ `, { encoding: "utf8", mode: 384 });
8505
8571
  }
8506
8572
  async function runInitManifest(config, options) {
8507
8573
  const manifestPath = expandPath(
@@ -11098,7 +11164,12 @@ async function main() {
11098
11164
  ).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
11099
11165
  await runUninstall(getRuntimeConfig(program2), options);
11100
11166
  });
11101
- program2.command("seed").description("Seed curated context from the manifest; never indexes whole repos by default").option("--dry-run", "Print files and ov commands without importing").option("--manifest <path>", "Manifest path for this seed run").action(async (options) => {
11167
+ program2.command("seed").description("Seed curated context from the manifest; never indexes whole repos by default").option("--dry-run", "Print files and ov commands without importing").option("--force", "Re-upload every candidate even if mtime+size match the recorded state").option("--manifest <path>", "Manifest path for this seed run").option(
11168
+ "--only <project>",
11169
+ "Restrict seeding to one or more manifest projects by name; repeat for multiple",
11170
+ collectOption,
11171
+ []
11172
+ ).action(async (options) => {
11102
11173
  await runSeed(getRuntimeConfig(program2, options.manifest), options);
11103
11174
  });
11104
11175
  program2.command("init-manifest").description("Create or update a per-developer seed manifest from one or more repo roots").option("--dry-run", "Print the manifest without writing it").option("--path <path>", "Manifest path; defaults to THREADNOTE_MANIFEST or ~/.openviking/seed-manifest.yaml").option("--replace", "Replace the manifest instead of merging with existing projects").option("--repo <path>", "Repo root to include; repeat for multiple repos", collectOption, []).action(async (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",