threadnote 0.6.1 → 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.
@@ -1191,14 +1191,14 @@ var require_util = __commonJS({
1191
1191
  });
1192
1192
  }
1193
1193
  exports2.useFunc = useFunc;
1194
- var Type;
1195
- (function(Type2) {
1196
- Type2[Type2["Num"] = 0] = "Num";
1197
- Type2[Type2["Str"] = 1] = "Str";
1198
- })(Type || (exports2.Type = Type = {}));
1194
+ var Type2;
1195
+ (function(Type3) {
1196
+ Type3[Type3["Num"] = 0] = "Num";
1197
+ Type3[Type3["Str"] = 1] = "Str";
1198
+ })(Type2 || (exports2.Type = Type2 = {}));
1199
1199
  function getErrorPath(dataProp, dataPropType, jsPropertySyntax) {
1200
1200
  if (dataProp instanceof codegen_1.Name) {
1201
- const isNumber = dataPropType === Type.Num;
1201
+ const isNumber = dataPropType === Type2.Num;
1202
1202
  return jsPropertySyntax ? isNumber ? (0, codegen_1._)`"[" + ${dataProp} + "]"` : (0, codegen_1._)`"['" + ${dataProp} + "']"` : isNumber ? (0, codegen_1._)`"/" + ${dataProp}` : (0, codegen_1._)`"/" + ${dataProp}.replace(/~/g, "~0").replace(/\\//g, "~1")`;
1203
1203
  }
1204
1204
  return jsPropertySyntax ? (0, codegen_1.getProperty)(dataProp).toString() : "/" + escapeJsonPointer(dataProp);
@@ -1499,37 +1499,37 @@ var require_dataType = __commonJS({
1499
1499
  DataType2[DataType2["Wrong"] = 1] = "Wrong";
1500
1500
  })(DataType || (exports2.DataType = DataType = {}));
1501
1501
  function getSchemaTypes(schema2) {
1502
- const types = getJSONTypes(schema2.type);
1503
- const hasNull = types.includes("null");
1502
+ const types2 = getJSONTypes(schema2.type);
1503
+ const hasNull = types2.includes("null");
1504
1504
  if (hasNull) {
1505
1505
  if (schema2.nullable === false)
1506
1506
  throw new Error("type: null contradicts nullable: false");
1507
1507
  } else {
1508
- if (!types.length && schema2.nullable !== void 0) {
1508
+ if (!types2.length && schema2.nullable !== void 0) {
1509
1509
  throw new Error('"nullable" cannot be used without "type"');
1510
1510
  }
1511
1511
  if (schema2.nullable === true)
1512
- types.push("null");
1512
+ types2.push("null");
1513
1513
  }
1514
- return types;
1514
+ return types2;
1515
1515
  }
1516
1516
  exports2.getSchemaTypes = getSchemaTypes;
1517
1517
  function getJSONTypes(ts) {
1518
- const types = Array.isArray(ts) ? ts : ts ? [ts] : [];
1519
- if (types.every(rules_1.isJSONType))
1520
- return types;
1521
- throw new Error("type must be JSONType or JSONType[]: " + types.join(","));
1518
+ const types2 = Array.isArray(ts) ? ts : ts ? [ts] : [];
1519
+ if (types2.every(rules_1.isJSONType))
1520
+ return types2;
1521
+ throw new Error("type must be JSONType or JSONType[]: " + types2.join(","));
1522
1522
  }
1523
1523
  exports2.getJSONTypes = getJSONTypes;
1524
- function coerceAndCheckDataType(it, types) {
1524
+ function coerceAndCheckDataType(it, types2) {
1525
1525
  const { gen, data, opts } = it;
1526
- const coerceTo = coerceToTypes(types, opts.coerceTypes);
1527
- const checkTypes = types.length > 0 && !(coerceTo.length === 0 && types.length === 1 && (0, applicability_1.schemaHasRulesForType)(it, types[0]));
1526
+ const coerceTo = coerceToTypes(types2, opts.coerceTypes);
1527
+ const checkTypes = types2.length > 0 && !(coerceTo.length === 0 && types2.length === 1 && (0, applicability_1.schemaHasRulesForType)(it, types2[0]));
1528
1528
  if (checkTypes) {
1529
- const wrongType = checkDataTypes(types, data, opts.strictNumbers, DataType.Wrong);
1529
+ const wrongType = checkDataTypes(types2, data, opts.strictNumbers, DataType.Wrong);
1530
1530
  gen.if(wrongType, () => {
1531
1531
  if (coerceTo.length)
1532
- coerceData(it, types, coerceTo);
1532
+ coerceData(it, types2, coerceTo);
1533
1533
  else
1534
1534
  reportTypeError(it);
1535
1535
  });
@@ -1538,15 +1538,15 @@ var require_dataType = __commonJS({
1538
1538
  }
1539
1539
  exports2.coerceAndCheckDataType = coerceAndCheckDataType;
1540
1540
  var COERCIBLE = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
1541
- function coerceToTypes(types, coerceTypes) {
1542
- return coerceTypes ? types.filter((t) => COERCIBLE.has(t) || coerceTypes === "array" && t === "array") : [];
1541
+ function coerceToTypes(types2, coerceTypes) {
1542
+ return coerceTypes ? types2.filter((t) => COERCIBLE.has(t) || coerceTypes === "array" && t === "array") : [];
1543
1543
  }
1544
- function coerceData(it, types, coerceTo) {
1544
+ function coerceData(it, types2, coerceTo) {
1545
1545
  const { gen, data, opts } = it;
1546
1546
  const dataType = gen.let("dataType", (0, codegen_1._)`typeof ${data}`);
1547
1547
  const coerced = gen.let("coerced", (0, codegen_1._)`undefined`);
1548
1548
  if (opts.coerceTypes === "array") {
1549
- gen.if((0, codegen_1._)`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => gen.assign(data, (0, codegen_1._)`${data}[0]`).assign(dataType, (0, codegen_1._)`typeof ${data}`).if(checkDataTypes(types, data, opts.strictNumbers), () => gen.assign(coerced, data)));
1549
+ gen.if((0, codegen_1._)`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () => gen.assign(data, (0, codegen_1._)`${data}[0]`).assign(dataType, (0, codegen_1._)`typeof ${data}`).if(checkDataTypes(types2, data, opts.strictNumbers), () => gen.assign(coerced, data)));
1550
1550
  }
1551
1551
  gen.if((0, codegen_1._)`${coerced} !== undefined`);
1552
1552
  for (const t of coerceTo) {
@@ -1622,19 +1622,19 @@ var require_dataType = __commonJS({
1622
1622
  return checkDataType(dataTypes[0], data, strictNums, correct);
1623
1623
  }
1624
1624
  let cond;
1625
- const types = (0, util_1.toHash)(dataTypes);
1626
- if (types.array && types.object) {
1625
+ const types2 = (0, util_1.toHash)(dataTypes);
1626
+ if (types2.array && types2.object) {
1627
1627
  const notObj = (0, codegen_1._)`typeof ${data} != "object"`;
1628
- cond = types.null ? notObj : (0, codegen_1._)`!${data} || ${notObj}`;
1629
- delete types.null;
1630
- delete types.array;
1631
- delete types.object;
1628
+ cond = types2.null ? notObj : (0, codegen_1._)`!${data} || ${notObj}`;
1629
+ delete types2.null;
1630
+ delete types2.array;
1631
+ delete types2.object;
1632
1632
  } else {
1633
1633
  cond = codegen_1.nil;
1634
1634
  }
1635
- if (types.number)
1636
- delete types.integer;
1637
- for (const t in types)
1635
+ if (types2.number)
1636
+ delete types2.integer;
1637
+ for (const t in types2)
1638
1638
  cond = (0, codegen_1.and)(cond, checkDataType(t, data, strictNums, correct));
1639
1639
  return cond;
1640
1640
  }
@@ -2439,9 +2439,9 @@ var require_validate = __commonJS({
2439
2439
  function typeAndKeywords(it, errsCount) {
2440
2440
  if (it.opts.jtd)
2441
2441
  return schemaKeywords(it, [], false, errsCount);
2442
- const types = (0, dataType_1.getSchemaTypes)(it.schema);
2443
- const checkedTypes = (0, dataType_1.coerceAndCheckDataType)(it, types);
2444
- schemaKeywords(it, types, !checkedTypes, errsCount);
2442
+ const types2 = (0, dataType_1.getSchemaTypes)(it.schema);
2443
+ const checkedTypes = (0, dataType_1.coerceAndCheckDataType)(it, types2);
2444
+ schemaKeywords(it, types2, !checkedTypes, errsCount);
2445
2445
  }
2446
2446
  function checkRefsAndKeywords(it) {
2447
2447
  const { schema: schema2, errSchemaPath, opts, self } = it;
@@ -2491,7 +2491,7 @@ var require_validate = __commonJS({
2491
2491
  if (items instanceof codegen_1.Name)
2492
2492
  gen.assign((0, codegen_1._)`${evaluated}.items`, items);
2493
2493
  }
2494
- function schemaKeywords(it, types, typeErrors, errsCount) {
2494
+ function schemaKeywords(it, types2, typeErrors, errsCount) {
2495
2495
  const { gen, schema: schema2, data, allErrors, opts, self } = it;
2496
2496
  const { RULES } = self;
2497
2497
  if (schema2.$ref && (opts.ignoreKeywordsWithRef || !(0, util_1.schemaHasRulesButRef)(schema2, RULES))) {
@@ -2499,7 +2499,7 @@ var require_validate = __commonJS({
2499
2499
  return;
2500
2500
  }
2501
2501
  if (!opts.jtd)
2502
- checkStrictTypes(it, types);
2502
+ checkStrictTypes(it, types2);
2503
2503
  gen.block(() => {
2504
2504
  for (const group of RULES.rules)
2505
2505
  groupKeywords(group);
@@ -2511,7 +2511,7 @@ var require_validate = __commonJS({
2511
2511
  if (group.type) {
2512
2512
  gen.if((0, dataType_2.checkDataType)(group.type, data, opts.strictNumbers));
2513
2513
  iterateKeywords(it, group);
2514
- if (types.length === 1 && types[0] === group.type && typeErrors) {
2514
+ if (types2.length === 1 && types2[0] === group.type && typeErrors) {
2515
2515
  gen.else();
2516
2516
  (0, dataType_2.reportTypeError)(it);
2517
2517
  }
@@ -2535,27 +2535,27 @@ var require_validate = __commonJS({
2535
2535
  }
2536
2536
  });
2537
2537
  }
2538
- function checkStrictTypes(it, types) {
2538
+ function checkStrictTypes(it, types2) {
2539
2539
  if (it.schemaEnv.meta || !it.opts.strictTypes)
2540
2540
  return;
2541
- checkContextTypes(it, types);
2541
+ checkContextTypes(it, types2);
2542
2542
  if (!it.opts.allowUnionTypes)
2543
- checkMultipleTypes(it, types);
2543
+ checkMultipleTypes(it, types2);
2544
2544
  checkKeywordTypes(it, it.dataTypes);
2545
2545
  }
2546
- function checkContextTypes(it, types) {
2547
- if (!types.length)
2546
+ function checkContextTypes(it, types2) {
2547
+ if (!types2.length)
2548
2548
  return;
2549
2549
  if (!it.dataTypes.length) {
2550
- it.dataTypes = types;
2550
+ it.dataTypes = types2;
2551
2551
  return;
2552
2552
  }
2553
- types.forEach((t) => {
2553
+ types2.forEach((t) => {
2554
2554
  if (!includesType(it.dataTypes, t)) {
2555
2555
  strictTypesError(it, `type "${t}" not allowed by context "${it.dataTypes.join(",")}"`);
2556
2556
  }
2557
2557
  });
2558
- narrowSchemaTypes(it, types);
2558
+ narrowSchemaTypes(it, types2);
2559
2559
  }
2560
2560
  function checkMultipleTypes(it, ts) {
2561
2561
  if (ts.length > 1 && !(ts.length === 2 && ts.includes("null"))) {
@@ -9556,9 +9556,9 @@ var ZodUnion = class extends ZodType {
9556
9556
  return this._def.options;
9557
9557
  }
9558
9558
  };
9559
- ZodUnion.create = (types, params) => {
9559
+ ZodUnion.create = (types2, params) => {
9560
9560
  return new ZodUnion({
9561
- options: types,
9561
+ options: types2,
9562
9562
  typeName: ZodFirstPartyTypeKind.ZodUnion,
9563
9563
  ...processCreateParams(params)
9564
9564
  });
@@ -21542,10 +21542,10 @@ function _property(property, schema2, params) {
21542
21542
  });
21543
21543
  }
21544
21544
  // @__NO_SIDE_EFFECTS__
21545
- function _mime(types, params) {
21545
+ function _mime(types2, params) {
21546
21546
  return new $ZodCheckMimeType({
21547
21547
  check: "mime_type",
21548
- mime: types,
21548
+ mime: types2,
21549
21549
  ...normalizeParams(params)
21550
21550
  });
21551
21551
  }
@@ -24603,7 +24603,7 @@ var ZodFile = /* @__PURE__ */ $constructor("ZodFile", (inst, def) => {
24603
24603
  inst._zod.processJSONSchema = (ctx, json3, params) => fileProcessor(inst, ctx, json3, params);
24604
24604
  inst.min = (size, params) => inst.check(_minSize(size, params));
24605
24605
  inst.max = (size, params) => inst.check(_maxSize(size, params));
24606
- inst.mime = (types, params) => inst.check(_mime(Array.isArray(types) ? types : [types], params));
24606
+ inst.mime = (types2, params) => inst.check(_mime(Array.isArray(types2) ? types2 : [types2], params));
24607
24607
  });
24608
24608
  function file(params) {
24609
24609
  return _file(ZodFile, params);
@@ -27777,15 +27777,15 @@ function parseUnionDef(def, refs) {
27777
27777
  return asAnyOf(def, refs);
27778
27778
  const options = def.options instanceof Map ? Array.from(def.options.values()) : def.options;
27779
27779
  if (options.every((x) => x._def.typeName in primitiveMappings && (!x._def.checks || !x._def.checks.length))) {
27780
- const types = options.reduce((types2, x) => {
27780
+ const types2 = options.reduce((types3, x) => {
27781
27781
  const type2 = primitiveMappings[x._def.typeName];
27782
- return type2 && !types2.includes(type2) ? [...types2, type2] : types2;
27782
+ return type2 && !types3.includes(type2) ? [...types3, type2] : types3;
27783
27783
  }, []);
27784
27784
  return {
27785
- type: types.length > 1 ? types : types[0]
27785
+ type: types2.length > 1 ? types2 : types2[0]
27786
27786
  };
27787
27787
  } else if (options.every((x) => x._def.typeName === "ZodLiteral" && !x.description)) {
27788
- const types = options.reduce((acc, x) => {
27788
+ const types2 = options.reduce((acc, x) => {
27789
27789
  const type2 = typeof x._def.value;
27790
27790
  switch (type2) {
27791
27791
  case "string":
@@ -27804,8 +27804,8 @@ function parseUnionDef(def, refs) {
27804
27804
  return acc;
27805
27805
  }
27806
27806
  }, []);
27807
- if (types.length === options.length) {
27808
- const uniqueTypes = types.filter((x, i, a) => a.indexOf(x) === i);
27807
+ if (types2.length === options.length) {
27808
+ const uniqueTypes = types2.filter((x, i, a) => a.indexOf(x) === i);
27809
27809
  return {
27810
27810
  type: uniqueTypes.length > 1 ? uniqueTypes : uniqueTypes[0],
27811
27811
  enum: options.reduce((acc, x) => {
@@ -30950,7 +30950,7 @@ var StdioServerTransport = class {
30950
30950
  };
30951
30951
 
30952
30952
  // src/mcp_server.ts
30953
- var import_promises3 = require("node:fs/promises");
30953
+ var import_promises4 = require("node:fs/promises");
30954
30954
  var import_node_os3 = require("node:os");
30955
30955
  var import_node_path3 = require("node:path");
30956
30956
 
@@ -30958,10 +30958,8 @@ var import_node_path3 = require("node:path");
30958
30958
  var DEFAULT_ACCOUNT = "local";
30959
30959
  var DEFAULT_AGENT_ID = "threadnote";
30960
30960
 
30961
- // src/share.ts
30961
+ // src/manifest.ts
30962
30962
  var import_promises2 = require("node:fs/promises");
30963
- var import_node_os2 = require("node:os");
30964
- var import_node_path2 = require("node:path");
30965
30963
 
30966
30964
  // node_modules/js-yaml/dist/js-yaml.mjs
30967
30965
  function isNothing(subject) {
@@ -33542,12 +33540,50 @@ function renamed(from, to) {
33542
33540
  throw new Error("Function yaml." + from + " is removed in js-yaml 4. Use yaml." + to + " instead, which is now safe by default.");
33543
33541
  };
33544
33542
  }
33543
+ var Type = type;
33544
+ var Schema = schema;
33545
+ var FAILSAFE_SCHEMA = failsafe;
33546
+ var JSON_SCHEMA = json2;
33547
+ var CORE_SCHEMA = core;
33548
+ var DEFAULT_SCHEMA = _default3;
33545
33549
  var load = loader.load;
33546
33550
  var loadAll = loader.loadAll;
33547
33551
  var dump = dumper.dump;
33552
+ var YAMLException = exception;
33553
+ var types = {
33554
+ binary,
33555
+ float,
33556
+ map: map2,
33557
+ null: _null4,
33558
+ pairs,
33559
+ set: set2,
33560
+ timestamp,
33561
+ bool,
33562
+ int: int2,
33563
+ merge: merge2,
33564
+ omap,
33565
+ seq,
33566
+ str
33567
+ };
33548
33568
  var safeLoad = renamed("safeLoad", "load");
33549
33569
  var safeLoadAll = renamed("safeLoadAll", "loadAll");
33550
33570
  var safeDump = renamed("safeDump", "dump");
33571
+ var jsYaml = {
33572
+ Type,
33573
+ Schema,
33574
+ FAILSAFE_SCHEMA,
33575
+ JSON_SCHEMA,
33576
+ CORE_SCHEMA,
33577
+ DEFAULT_SCHEMA,
33578
+ load,
33579
+ loadAll,
33580
+ dump,
33581
+ YAMLException,
33582
+ types,
33583
+ safeLoad,
33584
+ safeLoadAll,
33585
+ safeDump
33586
+ };
33551
33587
 
33552
33588
  // src/utils.ts
33553
33589
  var import_node_child_process = require("node:child_process");
@@ -33652,6 +33688,9 @@ function expandPath(path) {
33652
33688
  function getInvocationCwd() {
33653
33689
  return process.env.THREADNOTE_CALLER_CWD ?? process.cwd();
33654
33690
  }
33691
+ function trimTrailingSlash(value) {
33692
+ return value.endsWith("/") ? value.slice(0, -1) : value;
33693
+ }
33655
33694
  function sha256(content) {
33656
33695
  return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
33657
33696
  }
@@ -33725,6 +33764,74 @@ function uriSegment(value) {
33725
33764
  const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
33726
33765
  return normalized.length > 0 ? normalized : "unknown";
33727
33766
  }
33767
+ async function inferProjectFromQuery(manifestPath, query) {
33768
+ try {
33769
+ const manifest = await readSeedManifest(manifestPath);
33770
+ const normalized = query.toLowerCase();
33771
+ return manifest.projects.find((project) => normalized.includes(project.name.toLowerCase()));
33772
+ } catch {
33773
+ return void 0;
33774
+ }
33775
+ }
33776
+ async function readSeedManifest(path) {
33777
+ const raw = await (0, import_promises2.readFile)(path, "utf8");
33778
+ const loaded = jsYaml.load(raw);
33779
+ if (!isJsonObject(loaded)) {
33780
+ throw new Error(`Manifest must be an object: ${path}`);
33781
+ }
33782
+ const version2 = readNumber(loaded, "version");
33783
+ const projectsValue = loaded.projects;
33784
+ if (!Array.isArray(projectsValue)) {
33785
+ throw new Error(`Manifest projects must be an array: ${path}`);
33786
+ }
33787
+ const projects = [];
33788
+ for (const projectValue of projectsValue) {
33789
+ if (!isJsonObject(projectValue)) {
33790
+ throw new Error(`Manifest project must be an object: ${path}`);
33791
+ }
33792
+ const seed = readStringArray(projectValue, "seed");
33793
+ projects.push({
33794
+ name: readString(projectValue, "name"),
33795
+ path: readString(projectValue, "path"),
33796
+ seed,
33797
+ uri: readString(projectValue, "uri")
33798
+ });
33799
+ }
33800
+ let futureMonorepo;
33801
+ if (isJsonObject(loaded.future_monorepo)) {
33802
+ futureMonorepo = {
33803
+ pathCandidates: readStringArray(loaded.future_monorepo, "path_candidates"),
33804
+ uri: readString(loaded.future_monorepo, "uri")
33805
+ };
33806
+ }
33807
+ return { futureMonorepo, projects, version: version2 };
33808
+ }
33809
+ function readString(object3, key) {
33810
+ const value = object3[key];
33811
+ if (typeof value !== "string" || value.length === 0) {
33812
+ throw new Error(`Expected non-empty string for ${key}`);
33813
+ }
33814
+ return value;
33815
+ }
33816
+ function readNumber(object3, key) {
33817
+ const value = object3[key];
33818
+ if (typeof value !== "number") {
33819
+ throw new Error(`Expected number for ${key}`);
33820
+ }
33821
+ return value;
33822
+ }
33823
+ function readStringArray(object3, key) {
33824
+ const value = object3[key];
33825
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
33826
+ throw new Error(`Expected string array for ${key}`);
33827
+ }
33828
+ return value;
33829
+ }
33830
+
33831
+ // src/share.ts
33832
+ var import_promises3 = require("node:fs/promises");
33833
+ var import_node_os2 = require("node:os");
33834
+ var import_node_path2 = require("node:path");
33728
33835
 
33729
33836
  // src/runtime.ts
33730
33837
  function withIdentity(config2, args) {
@@ -33949,13 +34056,13 @@ async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun) {
33949
34056
  console.log(`Would run: ${formatShellCommand(ov, args)}`);
33950
34057
  return;
33951
34058
  }
33952
- const stagingDir = await (0, import_promises2.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "threadnote-share-"));
34059
+ const stagingDir = await (0, import_promises3.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "threadnote-share-"));
33953
34060
  const tempPath = (0, import_node_path2.join)(stagingDir, "body.txt");
33954
34061
  try {
33955
- await (0, import_promises2.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
34062
+ await (0, import_promises3.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
33956
34063
  await writeOvFileWithRetry(config2, ov, uri, tempPath, initialMode);
33957
34064
  } finally {
33958
- await (0, import_promises2.rm)(stagingDir, { force: true, recursive: true });
34065
+ await (0, import_promises3.rm)(stagingDir, { force: true, recursive: true });
33959
34066
  }
33960
34067
  }
33961
34068
  async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode) {
@@ -34051,7 +34158,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
34051
34158
  if (!relative2) {
34052
34159
  return;
34053
34160
  }
34054
- await (0, import_promises2.rm)((0, import_node_path2.join)(worktree, relative2), { force: true });
34161
+ await (0, import_promises3.rm)((0, import_node_path2.join)(worktree, relative2), { force: true });
34055
34162
  }
34056
34163
  async function removeMemoryUri(config2, ov, uri, dryRun) {
34057
34164
  const args = withIdentity(config2, ["rm", uri]);
@@ -34091,6 +34198,7 @@ async function main() {
34091
34198
  "Stdio MCP adapter for Threadnote shared local context.",
34092
34199
  "Prefer `recall_context` to find candidate viking:// URIs, then `read_context` files or `list_context` directories.",
34093
34200
  'Always pass JSON arguments. Example: recall_context({"query":"current repo latest handoff"}).',
34201
+ "recall_context also surfaces seeded project resources under viking://resources/repos/<project> when the query mentions a project from the seed manifest. See its tool description for the query convention.",
34094
34202
  "Older clients may use the compatibility aliases `search`, `read`, and `list`.",
34095
34203
  'For durable facts, store kind="durable"; for current work logs, store kind="handoff" with project/topic so Threadnote keeps one active memory updated.',
34096
34204
  "When a handoff describes an active branch or feature, recall durable feature memories for the same branch/topic before coding.",
@@ -34110,6 +34218,7 @@ function getRuntimeConfig() {
34110
34218
  account: process.env.THREADNOTE_ACCOUNT ?? DEFAULT_ACCOUNT,
34111
34219
  agentContextHome: expandPath(process.env.THREADNOTE_HOME ?? "~/.openviking"),
34112
34220
  agentId: process.env.THREADNOTE_AGENT_ID ?? DEFAULT_AGENT_ID,
34221
+ manifestPath: expandPath(process.env.THREADNOTE_MANIFEST ?? "~/.openviking/seed-manifest.yaml"),
34113
34222
  user: process.env.THREADNOTE_USER ?? process.env.USER ?? "unknown"
34114
34223
  };
34115
34224
  }
@@ -34118,13 +34227,13 @@ function registerTools(server, config2) {
34118
34227
  server,
34119
34228
  config2,
34120
34229
  "recall_context",
34121
- 'Search Threadnote context. Required: pass JSON arguments with a non-empty query, for example {"query":"unity-ui-ccc latest handoff"}.'
34230
+ `Search Threadnote context across personal memories and seeded project resources. Returns semantic hits from indexed Threadnote context (handoffs, durable feature memories, preferences, shared team memories) \u2014 and, when the query mentions a project name from the seed manifest, also from that project's seeded guidance (README, AGENTS.md, CLAUDE.md, SKILL.md, docs/**) under viking://resources/repos/<project>. Include the repo or project name in the query to make the project-guidance pass fire. Required: pass JSON arguments with a non-empty query, for example {"query":"unity-ui-ccc latest handoff"}.`
34122
34231
  );
34123
34232
  registerSearchTool(
34124
34233
  server,
34125
34234
  config2,
34126
34235
  "search",
34127
- "Compatibility alias for recall_context. Required: pass JSON arguments with a non-empty query."
34236
+ "Compatibility alias for recall_context. Searches both personal memories and seeded project resources; see recall_context for the query conventions."
34128
34237
  );
34129
34238
  registerReadTool(
34130
34239
  server,
@@ -34324,40 +34433,78 @@ function registerSearchTool(server, config2, name, description) {
34324
34433
  if (!checkedUri.ok) {
34325
34434
  return checkedUri.error;
34326
34435
  }
34327
- return runRecallTool(
34328
- config2,
34329
- [
34330
- "search",
34331
- checkedQuery.value,
34332
- ...checkedUri.value ? ["--uri", checkedUri.value] : [],
34333
- ...nodeLimit ? ["--node-limit", String(nodeLimit)] : []
34334
- ],
34335
- includeArchived === true
34336
- );
34436
+ return runRecallTool(config2, {
34437
+ query: checkedQuery.value,
34438
+ pinnedUri: checkedUri.value,
34439
+ nodeLimit,
34440
+ includeArchived: includeArchived === true
34441
+ });
34337
34442
  }
34338
34443
  );
34339
34444
  }
34340
- async function runRecallTool(config2, args, includeArchived) {
34341
- const semanticResult = await runOpenVikingTool(config2, args);
34445
+ async function runRecallTool(config2, params) {
34446
+ const baseArgs = [
34447
+ "search",
34448
+ params.query,
34449
+ ...params.pinnedUri ? ["--uri", params.pinnedUri] : [],
34450
+ ...params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : []
34451
+ ];
34452
+ const semanticResult = await runOpenVikingTool(config2, baseArgs);
34342
34453
  if (semanticResult.isError === true) {
34343
34454
  return semanticResult;
34344
34455
  }
34345
- const query = args[1];
34346
- const exactMatches = typeof query === "string" ? await exactMemoryMatchesText(config2, query, includeArchived) : void 0;
34347
- if (!exactMatches) {
34348
- return semanticResult;
34349
- }
34350
34456
  const [firstContent] = semanticResult.content;
34351
34457
  if (firstContent?.type !== "text") {
34352
34458
  return semanticResult;
34353
34459
  }
34354
- return {
34355
- ...semanticResult,
34356
- content: [{ type: "text", text: `${firstContent.text}
34357
-
34358
- Exact durable memory matches:
34359
- ${exactMatches}` }]
34360
- };
34460
+ const sections = [];
34461
+ if (firstContent.text) {
34462
+ sections.push(firstContent.text);
34463
+ }
34464
+ const seededSection = await seededResourcesSection(config2, params);
34465
+ if (seededSection) {
34466
+ sections.push(seededSection);
34467
+ }
34468
+ const exactMatches = await exactMemoryMatchesText(config2, params.query, params.includeArchived);
34469
+ if (exactMatches) {
34470
+ sections.push(`Exact durable memory matches:
34471
+ ${exactMatches}`);
34472
+ }
34473
+ if (sections.length <= 1) {
34474
+ return semanticResult;
34475
+ }
34476
+ return { ...semanticResult, content: [{ type: "text", text: sections.join("\n\n") }] };
34477
+ }
34478
+ async function seededResourcesSection(config2, params) {
34479
+ if (params.pinnedUri) {
34480
+ return void 0;
34481
+ }
34482
+ const project = await inferProjectFromQuery(config2.manifestPath, params.query);
34483
+ if (!project) {
34484
+ return void 0;
34485
+ }
34486
+ const projectResourceUri = trimTrailingSlash(project.uri);
34487
+ if (!projectResourceUri.startsWith("viking://")) {
34488
+ return void 0;
34489
+ }
34490
+ const ov = await requiredOpenVikingCli();
34491
+ const args = [
34492
+ "search",
34493
+ params.query,
34494
+ "--uri",
34495
+ projectResourceUri,
34496
+ ...params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : []
34497
+ ];
34498
+ const result = await runCommand(ov, withIdentity2(config2, args), { allowFailure: true });
34499
+ if (result.exitCode !== 0) {
34500
+ return void 0;
34501
+ }
34502
+ const body = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
34503
+ if (!body) {
34504
+ return void 0;
34505
+ }
34506
+ return `Seeded project resources (${projectResourceUri}):
34507
+ ${body}`;
34361
34508
  }
34362
34509
  async function exactMemoryMatchesText(config2, query, includeArchived) {
34363
34510
  const terms = exactRecallTerms(query);
@@ -34682,7 +34829,11 @@ function exactMemoryScopes(config2, includeArchived) {
34682
34829
  `${userBase}/incidents/active`,
34683
34830
  `${userBase}/events`,
34684
34831
  `${userBase}/shared`,
34685
- `viking://agent/${uriSegment2(config2.agentId)}/memories`
34832
+ `viking://agent/${uriSegment2(config2.agentId)}/memories`,
34833
+ // Seeded project resources live outside the user/memories tree. Include
34834
+ // them so exact-term grep surfaces matches in seeded READMEs, AGENTS.md,
34835
+ // SKILL.md, and docs/** alongside personal memory hits.
34836
+ "viking://resources/repos"
34686
34837
  ];
34687
34838
  return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
34688
34839
  }
@@ -34899,8 +35050,8 @@ async function requiredOpenVikingCli() {
34899
35050
  async function firstExistingPath(paths) {
34900
35051
  for (const path of paths) {
34901
35052
  try {
34902
- await (0, import_promises3.access)(path);
34903
- return await (0, import_promises3.realpath)(path);
35053
+ await (0, import_promises4.access)(path);
35054
+ return await (0, import_promises4.realpath)(path);
34904
35055
  } catch (_err) {
34905
35056
  continue;
34906
35057
  }
@@ -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;
@@ -7118,6 +7122,15 @@ function uriSegment(value) {
7118
7122
  const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
7119
7123
  return normalized.length > 0 ? normalized : "unknown";
7120
7124
  }
7125
+ async function inferProjectFromQuery(manifestPath, query) {
7126
+ try {
7127
+ const manifest = await readSeedManifest(manifestPath);
7128
+ const normalized = query.toLowerCase();
7129
+ return manifest.projects.find((project) => normalized.includes(project.name.toLowerCase()));
7130
+ } catch {
7131
+ return void 0;
7132
+ }
7133
+ }
7121
7134
  async function readSeedManifest(path) {
7122
7135
  const raw = await (0, import_promises3.readFile)(path, "utf8");
7123
7136
  const loaded = jsYaml.load(raw);
@@ -7388,11 +7401,32 @@ async function runRecall(config, options) {
7388
7401
  args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
7389
7402
  }
7390
7403
  await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
7404
+ await augmentRecallWithSeededResources(config, ov, options, inferredUri);
7391
7405
  await printExactMemoryMatches(config, ov, options.query, {
7392
7406
  dryRun: options.dryRun === true,
7393
7407
  includeArchived: options.includeArchived === true
7394
7408
  });
7395
7409
  }
7410
+ async function augmentRecallWithSeededResources(config, ov, options, inferredUri) {
7411
+ if (options.uri || options.inferScope === false) {
7412
+ return;
7413
+ }
7414
+ const project = await inferProjectFromQuery(config.manifestPath, options.query);
7415
+ if (!project) {
7416
+ return;
7417
+ }
7418
+ const projectResourceUri = trimTrailingSlash(project.uri);
7419
+ if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
7420
+ return;
7421
+ }
7422
+ const args = ["search", options.query, "--uri", projectResourceUri];
7423
+ if (options.nodeLimit) {
7424
+ args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
7425
+ }
7426
+ console.log(`
7427
+ Also searching seeded resources: ${projectResourceUri}`);
7428
+ await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
7429
+ }
7396
7430
  async function runRead(config, uri, options) {
7397
7431
  assertVikingUri(uri);
7398
7432
  const ov = await openVikingCliForMode(options.dryRun === true);
@@ -7511,21 +7545,11 @@ async function runImportPack(config, options) {
7511
7545
  );
7512
7546
  }
7513
7547
  async function inferRecallUri(config, query) {
7514
- const normalizedQuery = query.toLowerCase();
7515
- if (/\bskills?\b/.test(normalizedQuery)) {
7516
- const project2 = await inferProjectFromQuery(config, normalizedQuery);
7517
- return project2 ? `viking://resources/agent-skills/repo-local-${uriSegment(project2.name)}` : "viking://resources/agent-skills";
7518
- }
7519
- const project = await inferProjectFromQuery(config, normalizedQuery);
7520
- return project ? trimTrailingSlash(project.uri) : void 0;
7521
- }
7522
- async function inferProjectFromQuery(config, normalizedQuery) {
7523
- try {
7524
- const manifest = await readSeedManifest(config.manifestPath);
7525
- return manifest.projects.find((project) => normalizedQuery.includes(project.name.toLowerCase()));
7526
- } catch (_err) {
7548
+ if (!/\bskills?\b/.test(query.toLowerCase())) {
7527
7549
  return void 0;
7528
7550
  }
7551
+ const project = await inferProjectFromQuery(config.manifestPath, query);
7552
+ return project ? `viking://resources/agent-skills/repo-local-${uriSegment(project.name)}` : "viking://resources/agent-skills";
7529
7553
  }
7530
7554
  async function printExactMemoryMatches(config, ov, query, options) {
7531
7555
  const terms = exactRecallTerms(query);
@@ -7678,8 +7702,8 @@ async function removeVikingResourceWithRetry(ov, config, uri) {
7678
7702
  return false;
7679
7703
  }
7680
7704
  async function vikingResourceExists(ov, config, uri) {
7681
- const stat3 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
7682
- return stat3.exitCode === 0;
7705
+ const stat4 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
7706
+ return stat4.exitCode === 0;
7683
7707
  }
7684
7708
  async function ensureDurableMemoryDirectory(ov, config) {
7685
7709
  await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
@@ -7755,7 +7779,11 @@ function exactMemoryScopes(config, includeArchived) {
7755
7779
  `${userBase}/incidents/active`,
7756
7780
  `${userBase}/events`,
7757
7781
  `${userBase}/shared`,
7758
- `viking://agent/${uriSegment(config.agentId)}/memories`
7782
+ `viking://agent/${uriSegment(config.agentId)}/memories`,
7783
+ // Seeded project resources (READMEs, AGENTS.md, SKILL.md, docs/**) live
7784
+ // under viking://resources/repos/<project>. Include them so an exact-term
7785
+ // grep in a recall surfaces matches in seeded guidance, not only memories.
7786
+ "viking://resources/repos"
7759
7787
  ];
7760
7788
  return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
7761
7789
  }
@@ -8457,9 +8485,13 @@ async function runSeed(config, options) {
8457
8485
  const manifest = await readSeedManifest(config.manifestPath);
8458
8486
  const ignorePatterns = await loadIgnorePatterns();
8459
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);
8460
8491
  let importedCount = 0;
8461
8492
  let skippedCount = 0;
8462
- for (const project of manifest.projects) {
8493
+ let unchangedCount = 0;
8494
+ for (const project of projects) {
8463
8495
  const projectRoot = expandPath(project.path);
8464
8496
  if (!await exists(projectRoot)) {
8465
8497
  console.log(`WARN project missing: ${project.name} (${projectRoot})`);
@@ -8467,6 +8499,12 @@ async function runSeed(config, options) {
8467
8499
  }
8468
8500
  const candidates = await collectSeedCandidates(project, projectRoot, ignorePatterns);
8469
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
+ }
8470
8508
  const importPath = await prepareSeedFile(config, candidate, options.dryRun === true);
8471
8509
  if (!importPath) {
8472
8510
  skippedCount += 1;
@@ -8475,9 +8513,61 @@ async function runSeed(config, options) {
8475
8513
  const args = withIdentity(config, ["add-resource", importPath, "--to", candidate.destinationUri, "--wait"]);
8476
8514
  await maybeRun(options.dryRun === true, ov, args);
8477
8515
  importedCount += 1;
8516
+ if (fileStat && options.dryRun !== true) {
8517
+ state.files[candidate.destinationUri] = { mtimeMs: fileStat.mtimeMs, size: fileStat.size };
8518
+ }
8478
8519
  }
8479
8520
  }
8480
- console.log(`Seed complete: ${importedCount} candidate(s), ${skippedCount} skipped for safety.`);
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
+ }
8561
+ }
8562
+ return { files, version: 1 };
8563
+ } catch (_err) {
8564
+ return { files: {}, version: 1 };
8565
+ }
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 });
8481
8571
  }
8482
8572
  async function runInitManifest(config, options) {
8483
8573
  const manifestPath = expandPath(
@@ -11074,7 +11164,12 @@ async function main() {
11074
11164
  ).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
11075
11165
  await runUninstall(getRuntimeConfig(program2), options);
11076
11166
  });
11077
- 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) => {
11078
11173
  await runSeed(getRuntimeConfig(program2, options.manifest), options);
11079
11174
  });
11080
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) => {
@@ -21,9 +21,17 @@ At the start of a non-trivial task, recall relevant context before making change
21
21
  - the current repo and branch;
22
22
  - recent handoffs;
23
23
  - durable feature memories for the branch, feature name, project/topic, or issue;
24
+ - seeded project guidance under `viking://resources/repos/<project>` — README, AGENTS.md, CLAUDE.md, SKILL.md, docs/\*\*
25
+ — for canonical conventions, test commands, release process, on-call runbooks, and anything the team has checked into
26
+ the repo;
24
27
  - task-specific skills or workflow guidance;
25
28
  - user or team preferences that may affect the work.
26
29
 
30
+ Include the repo or project name as a token in the recall query so the project-guidance pass fires. `recall_context`
31
+ runs a parallel scoped search against `viking://resources/repos/<project>` whenever the query mentions a seeded project
32
+ name, so memories AND project documentation come back in the same response. Treat seeded resources as the canonical
33
+ source for "how does this repo do X" and personal memories as in-flight or per-author context layered on top.
34
+
27
35
  When a recalled handoff describes an active branch or feature, do a second recall for durable memories about that
28
36
  feature before coding. The handoff tells you the current work state; durable feature memories tell you the design,
29
37
  decisions, invariants, interfaces, and gotchas that should survive beyond one session.
package/docs/index.html CHANGED
@@ -78,7 +78,11 @@
78
78
  body {
79
79
  line-height: 1.55;
80
80
  font-size: clamp(15px, 1.05vw, 17px);
81
- overflow-x: hidden;
81
+ overflow-x: clip;
82
+ }
83
+
84
+ html {
85
+ overflow-x: clip;
82
86
  }
83
87
 
84
88
  ::selection {
@@ -117,6 +121,10 @@
117
121
  padding: 1em 1.2em;
118
122
  overflow-x: auto;
119
123
  line-height: 1.55;
124
+ /* Without this, a single long unbreakable line (curl URLs, etc.)
125
+ pushes the pre wider than its container and out of the viewport on
126
+ mobile. With it, the pre scrolls internally instead. */
127
+ max-width: 100%;
120
128
  }
121
129
 
122
130
  pre code {
@@ -148,6 +156,10 @@
148
156
  display: flex;
149
157
  flex-direction: column;
150
158
  justify-content: center;
159
+ /* Default flex/block intrinsic min-width is the widest child, which
160
+ lets long unbreakable mono lines (URLs, MCP calls) make the inner
161
+ wider than its declared 100%. Hard-cap it. */
162
+ min-width: 0;
151
163
  }
152
164
 
153
165
  .slide-eyebrow {
@@ -222,29 +234,14 @@
222
234
  align-items: flex-start;
223
235
  }
224
236
 
225
- .page-logo {
226
- position: fixed;
227
- top: 1.5rem;
228
- left: 1.5rem;
229
- width: 220px;
230
- height: 220px;
231
- z-index: 40;
232
- pointer-events: none;
233
- }
234
-
235
- .page-logo img {
236
- width: 100%;
237
- height: 100%;
237
+ .hero-logo {
238
238
  display: block;
239
- }
240
-
241
- @media (max-width: 720px) {
242
- .page-logo {
243
- width: 120px;
244
- height: 120px;
245
- top: 1rem;
246
- left: 1rem;
247
- }
239
+ width: clamp(140px, 18vw, 220px);
240
+ height: auto;
241
+ margin-bottom: 1.5rem;
242
+ /* Override `#hero .slide-inner { align-items: flex-start }` for just
243
+ this child so the logo sits centered above the left-aligned title. */
244
+ align-self: center;
248
245
  }
249
246
 
250
247
  .hero-title-gradient {
@@ -504,35 +501,89 @@
504
501
  border-radius: var(--radius-lg);
505
502
  padding: 1.5rem;
506
503
  display: grid;
507
- gap: 0.6rem;
504
+ gap: 0.75rem;
508
505
  transition: border-color 0.2s ease;
506
+ /* Let the grid track shrink to its container width rather than to the
507
+ widest mono child. Without this, long MCP call/response lines push
508
+ the grid item past viewport and the page scrolls horizontally on
509
+ mobile. */
510
+ min-width: 0;
509
511
  }
510
512
 
511
513
  .vignette:hover {
512
514
  border-color: var(--accent-line);
513
515
  }
514
516
 
515
- .vignette .v-prompt {
517
+ .vignette .v-say {
516
518
  font-style: italic;
519
+ font-size: 0.96rem;
517
520
  color: var(--text);
518
- font-size: 1rem;
521
+ background: rgba(255, 255, 255, 0.03);
522
+ border-left: 3px solid var(--accent);
523
+ border-radius: 4px;
524
+ padding: 0.55rem 0.85rem;
519
525
  }
520
526
 
521
- .vignette .v-answer {
527
+ .vignette .v-auto {
528
+ font-style: italic;
522
529
  color: var(--muted);
523
- font-size: 0.95rem;
524
530
  }
525
531
 
526
- .vignette .v-cmd {
532
+ .vignette pre.v-call,
533
+ .vignette pre.v-response {
527
534
  font-family: var(--font-mono);
528
- font-size: 0.82rem;
529
- color: var(--accent);
530
- padding: 0.45rem 0.75rem;
535
+ font-size: 0.78rem;
531
536
  background: var(--code-bg);
537
+ padding: 0.55rem 0.75rem;
532
538
  border-radius: 6px;
533
539
  border: 1px solid var(--line);
534
- white-space: nowrap;
540
+ border-left: 1px solid var(--line);
541
+ white-space: pre-wrap;
542
+ word-break: break-word;
535
543
  overflow-x: auto;
544
+ max-width: 100%;
545
+ line-height: 1.45;
546
+ margin: 0;
547
+ }
548
+
549
+ .vignette .v-call {
550
+ color: var(--accent);
551
+ }
552
+
553
+ .vignette .v-response {
554
+ color: var(--muted);
555
+ }
556
+
557
+ .vignette .v-say::before,
558
+ .vignette .v-call::before,
559
+ .vignette .v-response::before {
560
+ display: block;
561
+ font-family: var(--font-mono);
562
+ font-size: 0.68rem;
563
+ letter-spacing: 0.12em;
564
+ text-transform: uppercase;
565
+ color: var(--muted);
566
+ font-style: normal;
567
+ margin-bottom: 0.35rem;
568
+ }
569
+
570
+ .vignette .v-say::before {
571
+ content: 'You ask';
572
+ color: var(--accent);
573
+ }
574
+
575
+ .vignette .v-call::before {
576
+ content: 'Agent calls';
577
+ }
578
+
579
+ .vignette .v-response::before {
580
+ content: 'MCP returns';
581
+ }
582
+
583
+ .vignette .v-answer {
584
+ color: var(--muted);
585
+ font-size: 0.95rem;
586
+ margin-top: 0.25rem;
536
587
  }
537
588
 
538
589
  /* side nav dots */
@@ -695,10 +746,6 @@
695
746
  <body>
696
747
  <div class="progress"><div class="progress-bar" id="progressBar"></div></div>
697
748
 
698
- <div class="page-logo" aria-hidden="true">
699
- <img src="threadnote-logo-inverted.svg" alt="" />
700
- </div>
701
-
702
749
  <nav class="deck-nav" aria-label="Section progress">
703
750
  <a href="#hero" data-label="Intro"><span>Intro</span></a>
704
751
  <a href="#problem" data-label="Problem"><span>Problem</span></a>
@@ -710,8 +757,8 @@
710
757
  <a href="#seed" data-label="Seed"><span>Seed</span></a>
711
758
  <a href="#update" data-label="Update"><span>Update</span></a>
712
759
  <a href="#lifecycle" data-label="Lifecycle"><span>Lifecycle</span></a>
713
- <a href="#use-cases" data-label="Use cases"><span>Use cases</span></a>
714
760
  <a href="#share" data-label="Share"><span>Share</span></a>
761
+ <a href="#use-cases" data-label="Use cases"><span>Use cases</span></a>
715
762
  <a href="#close" data-label="Get started"><span>Start</span></a>
716
763
  </nav>
717
764
 
@@ -719,6 +766,7 @@
719
766
  <!-- 1. HERO -->
720
767
  <section id="hero" class="slide">
721
768
  <div class="slide-inner">
769
+ <img class="hero-logo" src="threadnote-logo-inverted.svg" alt="" aria-hidden="true" />
722
770
  <h1 class="hero-title-gradient">
723
771
  Shared local context<br />for <em class="hero-accent">development agents</em>.
724
772
  </h1>
@@ -727,7 +775,7 @@
727
775
  team-shared decisions — recallable from any session, on any agent, in any worktree.
728
776
  </p>
729
777
  <div class="cta-row">
730
- <span class="pill">v0.5.0 · local-first · MIT</span>
778
+ <span class="pill"><span id="version-tag">v0.6.1</span> · local-first · MIT</span>
731
779
  </div>
732
780
  </div>
733
781
  <div class="scroll-cue">scroll ↓ &nbsp;·&nbsp; <span class="kbd">↓</span> / <span class="kbd">j</span></div>
@@ -1254,75 +1302,10 @@ threadnote remember \
1254
1302
  </div>
1255
1303
  </section>
1256
1304
 
1257
- <!-- 11. USE CASES -->
1258
- <section id="use-cases" class="slide">
1259
- <div class="slide-inner">
1260
- <span class="slide-eyebrow">10 · Real-world use cases</span>
1261
- <h2>Where it actually pays off.</h2>
1262
-
1263
- <div class="vignettes">
1264
- <div class="vignette reveal">
1265
- <div class="v-prompt">"Continue the feature from yesterday."</div>
1266
- <div class="v-answer">
1267
- Fresh session, fresh agent. Threadnote recalls the active handoff for the branch plus the durable
1268
- feature memory: design decisions, the open PR, what's left, what just failed in CI. No re-explaining.
1269
- </div>
1270
- <div class="v-cmd">threadnote recall --query "&lt;branch&gt; latest handoff"</div>
1271
- </div>
1272
-
1273
- <div class="vignette reveal">
1274
- <div class="v-prompt">"What did we conclude about that prod-on-call thing?"</div>
1275
- <div class="v-answer">
1276
- The sanitized handoff from the on-call escalation is still there. Re-open the same Slack thread three
1277
- weeks later; the durable feature memory points your agent at the right files and the resolution.
1278
- </div>
1279
- <div class="v-cmd">threadnote recall --query "export workflow timeout on-call findings"</div>
1280
- </div>
1281
-
1282
- <div class="vignette reveal">
1283
- <div class="v-prompt">"Switch from Codex to Claude mid-task."</div>
1284
- <div class="v-answer">
1285
- Codex stores a handoff before pausing. Claude opens the same MCP layer, recalls the handoff plus the
1286
- durable memory, picks up the test command and the open blocker without you typing them again.
1287
- </div>
1288
- <div class="v-cmd">threadnote handoff --task "..." --next-step "..."</div>
1289
- </div>
1290
-
1291
- <div class="vignette reveal">
1292
- <div class="v-prompt">"Onboard a new teammate to a feature."</div>
1293
- <div class="v-answer">
1294
- Publish the feature's durable memory to the team repo with <code>share publish</code>. Their agent
1295
- pulls it on the next <code>share sync</code> and recall surfaces it alongside their own personal
1296
- memories — no DM forwarding, no PDF dumping.
1297
- </div>
1298
- <div class="v-cmd">threadnote share publish viking://user/&lt;you&gt;/memories/durable/...</div>
1299
- </div>
1300
-
1301
- <div class="vignette reveal">
1302
- <div class="v-prompt">"Compaction is about to nuke the context."</div>
1303
- <div class="v-answer">
1304
- Before context fills up, the agent stores a handoff. The next turn after compaction recalls it — the
1305
- summary covers the conversation arc, threadnote covers the durable facts the summary couldn't keep.
1306
- </div>
1307
- <div class="v-cmd">threadnote handoff --project &lt;repo&gt; --topic &lt;feature&gt; ...</div>
1308
- </div>
1309
-
1310
- <div class="vignette reveal">
1311
- <div class="v-prompt">"Where does this repo run its release notes?"</div>
1312
- <div class="v-answer">
1313
- You discovered it the hard way last time. Tell your agent to <code>remember</code> the fact. Next time
1314
- anyone asks, the agent finds it in two seconds without grepping the wiki.
1315
- </div>
1316
- <div class="v-cmd">threadnote remember --kind durable --topic release-process ...</div>
1317
- </div>
1318
- </div>
1319
- </div>
1320
- </section>
1321
-
1322
- <!-- 12. SHARE -->
1305
+ <!-- 11. SHARE -->
1323
1306
  <section id="share" class="slide">
1324
1307
  <div class="slide-inner">
1325
- <span class="slide-eyebrow">11 · Sharing across the team</span>
1308
+ <span class="slide-eyebrow">10 · Sharing across the team</span>
1326
1309
  <h2>Curated memories. Yours to push. Theirs to pull.</h2>
1327
1310
  <p class="lead">
1328
1311
  <code>threadnote share</code> publishes a subset of durable memories into a git repo so other engineers'
@@ -1409,6 +1392,81 @@ threadnote remember \
1409
1392
  </div>
1410
1393
  </section>
1411
1394
 
1395
+ <!-- 12. USE CASES -->
1396
+ <section id="use-cases" class="slide">
1397
+ <div class="slide-inner">
1398
+ <span class="slide-eyebrow">11 · Real-world use cases</span>
1399
+ <h2>What you say. What the agent does.</h2>
1400
+ <p class="lead">
1401
+ You don't run threadnote commands yourself. You talk to your agent the way you already do; the agent calls
1402
+ the MCP layer and the memory stays in sync across sessions, agents, and teammates.
1403
+ </p>
1404
+
1405
+ <div class="vignettes">
1406
+ <div class="vignette reveal">
1407
+ <div class="v-say">Continue where we left off on this branch.</div>
1408
+ <pre class="v-call">recall_context({query: "&lt;branch&gt; latest handoff durable feature memory"})</pre>
1409
+ <pre class="v-response">→ viking://user/you/memories/handoffs/active/&lt;branch&gt;/auto-precompact.md
1410
+ → viking://user/you/memories/durable/projects/&lt;repo&gt;/&lt;feature&gt;.md</pre>
1411
+ <div class="v-answer">
1412
+ Fresh session, fresh agent. It reads both pointers, picks up the design decisions, the open PR, what's
1413
+ left to test, and what just failed in CI. No re-explaining.
1414
+ </div>
1415
+ </div>
1416
+
1417
+ <div class="vignette reveal">
1418
+ <div class="v-say">Save where we are — I'm switching to another agent.</div>
1419
+ <pre class="v-call">remember_context({kind: "handoff", project: "&lt;repo&gt;", topic: "&lt;feature&gt;", text: "..."})</pre>
1420
+ <pre class="v-response">Stored: viking://user/you/memories/handoffs/active/&lt;repo&gt;/&lt;feature&gt;.md</pre>
1421
+ <div class="v-answer">
1422
+ Open the next agent — the SessionStart hook recalls the same handoff and the durable memory. It picks
1423
+ up the test command and the open blocker without you retyping them.
1424
+ </div>
1425
+ </div>
1426
+
1427
+ <div class="vignette reveal">
1428
+ <div class="v-say">Share this feature's design memory with the team.</div>
1429
+ <pre class="v-call">share_publish({uri: "viking://user/you/memories/durable/projects/&lt;repo&gt;/&lt;feature&gt;.md"})</pre>
1430
+ <pre class="v-response">Published → shared/&lt;team&gt;/durable/projects/&lt;repo&gt;/&lt;feature&gt;.md
1431
+ git push: ok</pre>
1432
+ <div class="v-answer">
1433
+ The memory lands in the team git repo. A teammate's next <code>share sync</code> pulls it; their
1434
+ recall surfaces it alongside their own memories — no DM forwarding, no PDF dumping.
1435
+ </div>
1436
+ </div>
1437
+
1438
+ <div class="vignette reveal">
1439
+ <div class="v-say v-auto">(automatic — the PreCompact hook fires before context compaction)</div>
1440
+ <pre class="v-call">remember_context({kind: "handoff", project: "&lt;repo&gt;", topic: "auto-precompact", text: "..."})</pre>
1441
+ <pre class="v-response">Stored: viking://user/you/memories/handoffs/active/&lt;repo&gt;/auto-precompact.md</pre>
1442
+ <div class="v-answer">
1443
+ Compaction summarizes the conversation arc but loses the durable facts. Threadnote's hook captures them
1444
+ first; the post-compaction turn recalls them and resumes without amnesia.
1445
+ </div>
1446
+ </div>
1447
+
1448
+ <div class="vignette reveal">
1449
+ <div class="v-say">Remember that this repo cuts release notes from CI via gh release create.</div>
1450
+ <pre class="v-call">remember_context({kind: "durable", project: "&lt;repo&gt;", topic: "release-process", text: "..."})</pre>
1451
+ <pre class="v-response">Stored: viking://user/you/memories/durable/projects/&lt;repo&gt;/release-process.md</pre>
1452
+ <div class="v-answer">
1453
+ Next time anyone asks where releases happen, the agent finds it in two seconds. No grepping the wiki.
1454
+ </div>
1455
+ </div>
1456
+
1457
+ <div class="vignette reveal">
1458
+ <div class="v-say">What did we conclude about the export-workflow timeout?</div>
1459
+ <pre class="v-call">recall_context({query: "export workflow timeout findings"})</pre>
1460
+ <pre class="v-response">→ viking://user/you/memories/durable/projects/&lt;repo&gt;/export-timeout.md (score 0.71)</pre>
1461
+ <div class="v-answer">
1462
+ Re-open the same investigation three weeks later; the durable memory points your agent at the right
1463
+ files and the resolution. The trail doesn't decay.
1464
+ </div>
1465
+ </div>
1466
+ </div>
1467
+ </div>
1468
+ </section>
1469
+
1412
1470
  <!-- 13. CLOSE -->
1413
1471
  <section id="close" class="slide">
1414
1472
  <div class="slide-inner">
@@ -1538,6 +1596,27 @@ threadnote doctor --dry-run</code></pre>
1538
1596
  }
1539
1597
  });
1540
1598
  })();
1599
+
1600
+ // Replace the hardcoded version pill with whatever npm currently
1601
+ // serves as latest. Fails silently and keeps the fallback text on any
1602
+ // network or registry error — the deck must render even offline.
1603
+ (async () => {
1604
+ const tag = document.getElementById('version-tag');
1605
+ if (!tag) return;
1606
+ try {
1607
+ const response = await fetch('https://registry.npmjs.org/threadnote/latest', {
1608
+ headers: {accept: 'application/json'},
1609
+ signal: AbortSignal.timeout(3000),
1610
+ });
1611
+ if (!response.ok) return;
1612
+ const data = await response.json();
1613
+ if (typeof data.version === 'string' && data.version.length > 0) {
1614
+ tag.textContent = 'v' + data.version;
1615
+ }
1616
+ } catch {
1617
+ // Keep the static fallback.
1618
+ }
1619
+ })();
1541
1620
  </script>
1542
1621
  </body>
1543
1622
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.6.1",
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",