threadnote 1.1.6 → 1.3.0
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/.threadnoteignore +2 -0
- package/README.md +8 -0
- package/dist/mcp_server.cjs +2291 -1038
- package/dist/threadnote.cjs +1195 -45
- package/docs/index.html +11 -0
- package/docs/share.md +102 -0
- package/package.json +1 -1
package/dist/mcp_server.cjs
CHANGED
|
@@ -36193,6 +36193,7 @@ function grepUrisFromJson(output) {
|
|
|
36193
36193
|
}
|
|
36194
36194
|
}
|
|
36195
36195
|
var RECALL_CATEGORY_ORDER = ["memories", "resources", "skills"];
|
|
36196
|
+
var RECALL_PROMOTED_EXACT_SCORE = 0;
|
|
36196
36197
|
function recallSnippet(value) {
|
|
36197
36198
|
if (typeof value !== "string") {
|
|
36198
36199
|
return "";
|
|
@@ -36244,11 +36245,14 @@ function parseRecallHits(output, options = {}) {
|
|
|
36244
36245
|
return [];
|
|
36245
36246
|
}
|
|
36246
36247
|
}
|
|
36248
|
+
function stripAnchor(uri) {
|
|
36249
|
+
return uri.replace(/#.*$/, "");
|
|
36250
|
+
}
|
|
36247
36251
|
function mergeRecallHits(passes) {
|
|
36248
36252
|
const byDocument = /* @__PURE__ */ new Map();
|
|
36249
36253
|
for (const pass of passes) {
|
|
36250
36254
|
for (const hit of pass) {
|
|
36251
|
-
const documentUri = hit.uri
|
|
36255
|
+
const documentUri = stripAnchor(hit.uri);
|
|
36252
36256
|
const existing = byDocument.get(documentUri);
|
|
36253
36257
|
if (!existing || hit.score > existing.score) {
|
|
36254
36258
|
byDocument.set(documentUri, { ...hit, uri: documentUri });
|
|
@@ -36256,23 +36260,120 @@ function mergeRecallHits(passes) {
|
|
|
36256
36260
|
}
|
|
36257
36261
|
}
|
|
36258
36262
|
return [...byDocument.values()].sort(
|
|
36259
|
-
(left, right) =>
|
|
36263
|
+
(left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || right.score - left.score
|
|
36264
|
+
);
|
|
36265
|
+
}
|
|
36266
|
+
function recallCategoryRank(category) {
|
|
36267
|
+
const index = RECALL_CATEGORY_ORDER.indexOf(category);
|
|
36268
|
+
return index === -1 ? RECALL_CATEGORY_ORDER.length : index;
|
|
36269
|
+
}
|
|
36270
|
+
function dedupeByContent(hits) {
|
|
36271
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36272
|
+
const kept = [];
|
|
36273
|
+
for (const hit of hits) {
|
|
36274
|
+
if (hit.category !== "memories" && hit.snippet.length > 0) {
|
|
36275
|
+
const key = `${hit.category}
|
|
36276
|
+
${hit.snippet}`;
|
|
36277
|
+
if (seen.has(key)) {
|
|
36278
|
+
continue;
|
|
36279
|
+
}
|
|
36280
|
+
seen.add(key);
|
|
36281
|
+
}
|
|
36282
|
+
kept.push(hit);
|
|
36283
|
+
}
|
|
36284
|
+
return kept;
|
|
36285
|
+
}
|
|
36286
|
+
function categoryForUri(uri) {
|
|
36287
|
+
if (uri.includes("/memories/")) {
|
|
36288
|
+
return "memories";
|
|
36289
|
+
}
|
|
36290
|
+
if (uri.startsWith("viking://resources/agent-skills/")) {
|
|
36291
|
+
return "skills";
|
|
36292
|
+
}
|
|
36293
|
+
return "resources";
|
|
36294
|
+
}
|
|
36295
|
+
function contextTypeForCategory(category) {
|
|
36296
|
+
if (category === "memories") {
|
|
36297
|
+
return "memory";
|
|
36298
|
+
}
|
|
36299
|
+
return category === "skills" ? "skill" : "resource";
|
|
36300
|
+
}
|
|
36301
|
+
function applyExactMatchBoost(hits, exactMatches) {
|
|
36302
|
+
if (exactMatches.length === 0) {
|
|
36303
|
+
return hits;
|
|
36304
|
+
}
|
|
36305
|
+
const termsByUri = new Map(exactMatches.map((match) => [stripAnchor(match.uri), match.terms]));
|
|
36306
|
+
const annotated = hits.map((hit) => {
|
|
36307
|
+
const terms = termsByUri.get(stripAnchor(hit.uri));
|
|
36308
|
+
return terms ? { ...hit, exactTerms: terms } : hit;
|
|
36309
|
+
});
|
|
36310
|
+
const present = new Set(annotated.map((hit) => stripAnchor(hit.uri)));
|
|
36311
|
+
const promoted = [...termsByUri.keys()].filter((uri) => !present.has(uri)).map((uri) => {
|
|
36312
|
+
const category = categoryForUri(uri);
|
|
36313
|
+
return {
|
|
36314
|
+
category,
|
|
36315
|
+
contextType: contextTypeForCategory(category),
|
|
36316
|
+
exactTerms: termsByUri.get(uri) ?? [],
|
|
36317
|
+
score: RECALL_PROMOTED_EXACT_SCORE,
|
|
36318
|
+
snippet: "",
|
|
36319
|
+
uri
|
|
36320
|
+
};
|
|
36321
|
+
});
|
|
36322
|
+
return [...annotated, ...promoted].sort(
|
|
36323
|
+
(left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (right.exactTerms?.length ?? 0) - (left.exactTerms?.length ?? 0) || right.score - left.score
|
|
36260
36324
|
);
|
|
36261
36325
|
}
|
|
36262
|
-
function
|
|
36263
|
-
if (
|
|
36326
|
+
function renderRecallHits(shown, overflow) {
|
|
36327
|
+
if (shown.length === 0) {
|
|
36264
36328
|
return void 0;
|
|
36265
36329
|
}
|
|
36266
|
-
const shown = hits.slice(0, maxHits);
|
|
36267
36330
|
const lines = shown.flatMap((hit, index) => {
|
|
36268
|
-
const
|
|
36331
|
+
const scorePart = hit.score > 0 ? `score ${hit.score.toFixed(2)}` : void 0;
|
|
36332
|
+
const exactPart = hit.exactTerms?.length ? `exact: ${hit.exactTerms.join(", ")}` : void 0;
|
|
36333
|
+
const head = `${index + 1}. ${[hit.contextType, scorePart, exactPart].filter(Boolean).join(" \xB7 ")} \xB7 ${hit.uri}`;
|
|
36269
36334
|
return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
|
|
36270
36335
|
});
|
|
36271
|
-
if (
|
|
36272
|
-
lines.push(`(+${
|
|
36336
|
+
if (overflow > 0) {
|
|
36337
|
+
lines.push(`(+${overflow} more \u2014 refine the query or read a URI above)`);
|
|
36273
36338
|
}
|
|
36274
36339
|
return lines.join("\n");
|
|
36275
36340
|
}
|
|
36341
|
+
var RECALL_CATEGORY_RESERVE = 2;
|
|
36342
|
+
function selectShownHits(ranked, limit, reserve) {
|
|
36343
|
+
if (ranked.length <= limit) {
|
|
36344
|
+
return ranked;
|
|
36345
|
+
}
|
|
36346
|
+
const selected = /* @__PURE__ */ new Set();
|
|
36347
|
+
for (const category of RECALL_CATEGORY_ORDER) {
|
|
36348
|
+
let taken = 0;
|
|
36349
|
+
for (const hit of ranked) {
|
|
36350
|
+
if (selected.size >= limit || taken >= reserve) {
|
|
36351
|
+
break;
|
|
36352
|
+
}
|
|
36353
|
+
if (hit.category === category && !selected.has(hit.uri)) {
|
|
36354
|
+
selected.add(hit.uri);
|
|
36355
|
+
taken += 1;
|
|
36356
|
+
}
|
|
36357
|
+
}
|
|
36358
|
+
}
|
|
36359
|
+
for (const hit of ranked) {
|
|
36360
|
+
if (selected.size >= limit) {
|
|
36361
|
+
break;
|
|
36362
|
+
}
|
|
36363
|
+
selected.add(hit.uri);
|
|
36364
|
+
}
|
|
36365
|
+
return ranked.filter((hit) => selected.has(hit.uri));
|
|
36366
|
+
}
|
|
36367
|
+
function buildRecallSections(passes, exactMatches, limit) {
|
|
36368
|
+
const ranked = dedupeByContent(applyExactMatchBoost(mergeRecallHits(passes), exactMatches));
|
|
36369
|
+
const shown = selectShownHits(ranked, limit, RECALL_CATEGORY_RESERVE);
|
|
36370
|
+
const shownUris = new Set(shown.map((hit) => stripAnchor(hit.uri)));
|
|
36371
|
+
return {
|
|
36372
|
+
exactTail: formatExactMatchPointers(exactMatches.filter((match) => !shownUris.has(stripAnchor(match.uri)))),
|
|
36373
|
+
ranked,
|
|
36374
|
+
semanticSection: renderRecallHits(shown, ranked.length - shown.length)
|
|
36375
|
+
};
|
|
36376
|
+
}
|
|
36276
36377
|
function exactMemoryScopeUris(params) {
|
|
36277
36378
|
const { agentMemoriesUri, includeArchived, intents, projectName, projectResourceUri, userBase } = params;
|
|
36278
36379
|
const durable = projectName ? `${userBase}/durable/projects/${projectName}` : `${userBase}/durable/projects`;
|
|
@@ -36719,835 +36820,1126 @@ function indexRepairWarning(uri, result) {
|
|
|
36719
36820
|
return `${uri}: ${result.stderr.trim() || result.stdout.trim() || "reindex failed"}`;
|
|
36720
36821
|
}
|
|
36721
36822
|
|
|
36722
|
-
// src/
|
|
36823
|
+
// src/share.ts
|
|
36824
|
+
var import_promises4 = require("node:fs/promises");
|
|
36825
|
+
var import_node_os2 = require("node:os");
|
|
36723
36826
|
var import_node_path3 = require("node:path");
|
|
36724
|
-
var
|
|
36725
|
-
var
|
|
36726
|
-
|
|
36727
|
-
|
|
36728
|
-
|
|
36729
|
-
|
|
36730
|
-
|
|
36731
|
-
|
|
36732
|
-
|
|
36733
|
-
|
|
36734
|
-
|
|
36735
|
-
|
|
36736
|
-
|
|
36737
|
-
|
|
36738
|
-
|
|
36739
|
-
|
|
36740
|
-
|
|
36741
|
-
|
|
36827
|
+
var import_node_util = require("node:util");
|
|
36828
|
+
var TEAMS_FILE_VERSION = 1;
|
|
36829
|
+
var SHARED_SEGMENT = "shared";
|
|
36830
|
+
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
36831
|
+
var SHAREABLE_ARTIFACT_DIR = "agent-artifacts";
|
|
36832
|
+
var SHAREABLE_TOP_LEVEL_DIRS = [...SHAREABLE_MEMORY_KIND_DIRS, SHAREABLE_ARTIFACT_DIR];
|
|
36833
|
+
var ARTIFACT_INSTALL_METADATA_VERSION = 1;
|
|
36834
|
+
var BUNDLE_MANIFEST_VERSION = 1;
|
|
36835
|
+
var BUNDLE_MANIFEST_FILE = ".threadnote-bundle.json";
|
|
36836
|
+
var BUNDLE_INSTALL_METADATA_FILE = ".threadnote-bundle-install.json";
|
|
36837
|
+
var OV_SUMMARY_FILES = [".abstract.md", ".overview.md"];
|
|
36838
|
+
var BUNDLE_IGNORE_DIR_NAMES = [".git", "node_modules", "reviews", "repos"];
|
|
36839
|
+
var PACK_INDEX_SUFFIX = ".pack.md";
|
|
36840
|
+
var PACK_MANIFEST_SUFFIX = ".pack.json";
|
|
36841
|
+
var PACK_FILES_DIR = "files";
|
|
36842
|
+
var PACK_ROOT_TOKEN = "${THREADNOTE_PACK_ROOT}";
|
|
36843
|
+
var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
36844
|
+
var DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
36845
|
+
var SCRUBBER_PATTERNS = [
|
|
36846
|
+
// Credentials: never redactable. Blocking is the only safe response —
|
|
36847
|
+
// automated redaction risks false negatives that leave material in git.
|
|
36848
|
+
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
36849
|
+
{ name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
36850
|
+
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
36851
|
+
{ name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
|
|
36852
|
+
{ name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
|
|
36853
|
+
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
36854
|
+
// Matches bare JWTs (three base64url segments). May surface a JWE token in
|
|
36855
|
+
// legitimate docs; if that becomes noisy we can switch to warn-only.
|
|
36856
|
+
{ name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
36857
|
+
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
36858
|
+
// Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
|
|
36859
|
+
// xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
|
|
36860
|
+
{ name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i },
|
|
36861
|
+
// Soft leaks: block by default (so the agent sees them and decides), but
|
|
36862
|
+
// allow opt-in redaction so curated memories with incidental matches can
|
|
36863
|
+
// ship without a manual rewrite. Local home paths are the recurring
|
|
36864
|
+
// real-world leak; the regexes greedily consume the whole path segment
|
|
36865
|
+
// (including subdirectories) up to whitespace or common closing punctuation
|
|
36866
|
+
// so redaction collapses an entire path to a single placeholder rather than
|
|
36867
|
+
// leaving the subpath visible.
|
|
36868
|
+
{ name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
|
|
36869
|
+
// No leading \b: it only matches when a word char precedes the slash, which
|
|
36870
|
+
// misses the common cases (after =, :, whitespace, or line start). Mirror the
|
|
36871
|
+
// macOS pattern so /home/... is caught in every position.
|
|
36872
|
+
{ name: "linux home path", placeholder: "<local-path>", regex: /\/home\/[^\s)>"'`,]+/ }
|
|
36873
|
+
];
|
|
36874
|
+
function applyScrubber(content, { redact }) {
|
|
36875
|
+
let cleaned = content;
|
|
36876
|
+
const redactions = [];
|
|
36877
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
36878
|
+
if (!pattern.regex.test(cleaned)) {
|
|
36879
|
+
continue;
|
|
36880
|
+
}
|
|
36881
|
+
if (!pattern.placeholder || !redact) {
|
|
36882
|
+
return { blocker: pattern.name, cleaned: content, redactions: [] };
|
|
36883
|
+
}
|
|
36884
|
+
const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`;
|
|
36885
|
+
const globalRegex = new RegExp(pattern.regex.source, flags);
|
|
36886
|
+
const matches = cleaned.match(globalRegex) ?? [];
|
|
36887
|
+
cleaned = cleaned.replace(globalRegex, pattern.placeholder);
|
|
36888
|
+
redactions.push({ count: matches.length, name: pattern.name });
|
|
36742
36889
|
}
|
|
36743
|
-
return {
|
|
36744
|
-
body,
|
|
36745
|
-
content: trimmed,
|
|
36746
|
-
headerTitle: firstLine,
|
|
36747
|
-
metadata: {
|
|
36748
|
-
archivedFrom: headerValue(header, "archived_from"),
|
|
36749
|
-
kind,
|
|
36750
|
-
project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
|
|
36751
|
-
sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
|
|
36752
|
-
status,
|
|
36753
|
-
supersedes: headerValue(header, "supersedes"),
|
|
36754
|
-
timestamp: headerValue(header, "timestamp") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
36755
|
-
topic: normalizeOptionalMetadata(headerValue(header, "topic"))
|
|
36756
|
-
},
|
|
36757
|
-
uri
|
|
36758
|
-
};
|
|
36890
|
+
return { cleaned, redactions };
|
|
36759
36891
|
}
|
|
36760
|
-
|
|
36761
|
-
|
|
36762
|
-
const
|
|
36763
|
-
|
|
36764
|
-
|
|
36765
|
-
groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
|
|
36892
|
+
var autoShareStates = /* @__PURE__ */ new Map();
|
|
36893
|
+
function startShareBackgroundFetch(config2) {
|
|
36894
|
+
const state = autoShareState(config2);
|
|
36895
|
+
if (state.timer) {
|
|
36896
|
+
return;
|
|
36766
36897
|
}
|
|
36767
|
-
|
|
36768
|
-
|
|
36769
|
-
|
|
36770
|
-
|
|
36771
|
-
|
|
36772
|
-
|
|
36773
|
-
|
|
36774
|
-
|
|
36775
|
-
|
|
36776
|
-
|
|
36777
|
-
|
|
36898
|
+
void refreshShareUpdateState(config2, { force: true }).catch((err) => {
|
|
36899
|
+
console.error(`share auto-fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
36900
|
+
});
|
|
36901
|
+
state.timer = setInterval(() => {
|
|
36902
|
+
void refreshShareUpdateState(config2, { force: false }).catch((err) => {
|
|
36903
|
+
console.error(`share auto-fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
36904
|
+
});
|
|
36905
|
+
}, AUTO_SHARE_FETCH_INTERVAL_MS);
|
|
36906
|
+
state.timer.unref?.();
|
|
36907
|
+
}
|
|
36908
|
+
async function syncSharedReposBeforeAgentRead(config2) {
|
|
36909
|
+
const state = autoShareState(config2);
|
|
36910
|
+
return enqueueShareOperation(state, async () => {
|
|
36911
|
+
await loadPendingReindexes(config2, state);
|
|
36912
|
+
const warnings = await refreshShareUpdateStateLocked(config2, state, { force: false });
|
|
36913
|
+
const syncTeams = /* @__PURE__ */ new Set([...state.behindTeams, ...state.pendingReindexes.keys()]);
|
|
36914
|
+
if (syncTeams.size === 0) {
|
|
36915
|
+
return { syncedTeams: [], warnings };
|
|
36778
36916
|
}
|
|
36779
|
-
const
|
|
36780
|
-
const
|
|
36781
|
-
const
|
|
36782
|
-
|
|
36783
|
-
|
|
36784
|
-
|
|
36785
|
-
|
|
36786
|
-
|
|
36787
|
-
|
|
36788
|
-
|
|
36789
|
-
|
|
36790
|
-
}
|
|
36791
|
-
|
|
36792
|
-
|
|
36793
|
-
|
|
36794
|
-
duplicateKeep,
|
|
36795
|
-
duplicateGroup.map((record2) => record2.uri)
|
|
36796
|
-
),
|
|
36797
|
-
reason: "keep exact duplicate group with source URIs",
|
|
36798
|
-
sourceUris: duplicateGroup.map((record2) => record2.uri),
|
|
36799
|
-
uri: duplicateKeep.uri
|
|
36800
|
-
});
|
|
36917
|
+
const syncedTeams = [];
|
|
36918
|
+
const remainingBehind = new Set(state.behindTeams);
|
|
36919
|
+
for (const team of syncTeams) {
|
|
36920
|
+
try {
|
|
36921
|
+
const warning2 = await runShareSyncQuiet(config2, state, { team });
|
|
36922
|
+
if (warning2) {
|
|
36923
|
+
warnings.push(warning2);
|
|
36924
|
+
} else {
|
|
36925
|
+
remainingBehind.delete(team);
|
|
36926
|
+
syncedTeams.push(team);
|
|
36927
|
+
}
|
|
36928
|
+
} catch (err) {
|
|
36929
|
+
warnings.push(
|
|
36930
|
+
`Auto-sync for shared team "${team}" failed: ${err instanceof Error ? err.message : String(err)}`
|
|
36931
|
+
);
|
|
36801
36932
|
}
|
|
36802
36933
|
}
|
|
36803
|
-
|
|
36804
|
-
|
|
36805
|
-
|
|
36806
|
-
|
|
36807
|
-
|
|
36934
|
+
state.behindTeams = remainingBehind;
|
|
36935
|
+
state.lastCheckedAt = Date.now();
|
|
36936
|
+
return { syncedTeams, warnings };
|
|
36937
|
+
});
|
|
36938
|
+
}
|
|
36939
|
+
function autoShareState(config2) {
|
|
36940
|
+
const key = `${config2.agentContextHome}:${config2.account}:${config2.user}`;
|
|
36941
|
+
let state = autoShareStates.get(key);
|
|
36942
|
+
if (!state) {
|
|
36943
|
+
state = { behindTeams: /* @__PURE__ */ new Set(), lastCheckedAt: 0, pendingReindexes: /* @__PURE__ */ new Map() };
|
|
36944
|
+
autoShareStates.set(key, state);
|
|
36945
|
+
}
|
|
36946
|
+
return state;
|
|
36947
|
+
}
|
|
36948
|
+
function pendingReindexesPath(config2) {
|
|
36949
|
+
return (0, import_node_path3.join)(config2.agentContextHome, "share", "auto-sync-pending-reindexes.json");
|
|
36950
|
+
}
|
|
36951
|
+
async function loadPendingReindexes(config2, state) {
|
|
36952
|
+
const raw = await readFileIfExists(pendingReindexesPath(config2));
|
|
36953
|
+
if (!raw) {
|
|
36954
|
+
state.pendingReindexes = /* @__PURE__ */ new Map();
|
|
36955
|
+
return;
|
|
36956
|
+
}
|
|
36957
|
+
const parsed = parseJsonConfigObject(raw);
|
|
36958
|
+
if (!parsed || typeof parsed.teams !== "object" || parsed.teams === null || Array.isArray(parsed.teams)) {
|
|
36959
|
+
state.pendingReindexes = /* @__PURE__ */ new Map();
|
|
36960
|
+
return;
|
|
36961
|
+
}
|
|
36962
|
+
const pending = /* @__PURE__ */ new Map();
|
|
36963
|
+
for (const [team, value] of Object.entries(parsed.teams)) {
|
|
36964
|
+
if (!Array.isArray(value)) {
|
|
36808
36965
|
continue;
|
|
36809
36966
|
}
|
|
36810
|
-
|
|
36811
|
-
|
|
36812
|
-
if (
|
|
36967
|
+
const changes = [];
|
|
36968
|
+
for (const item of value) {
|
|
36969
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
36813
36970
|
continue;
|
|
36814
36971
|
}
|
|
36815
|
-
|
|
36816
|
-
|
|
36817
|
-
|
|
36818
|
-
reason: "strip self-supersedes header",
|
|
36819
|
-
sourceUris: [record2.uri],
|
|
36820
|
-
uri: record2.uri
|
|
36821
|
-
});
|
|
36822
|
-
}
|
|
36823
|
-
if (isStaleLookingHandoff(record2, now)) {
|
|
36824
|
-
manualReview.push({ reason: "stale-looking active handoff", uri: record2.uri });
|
|
36972
|
+
const entry = item;
|
|
36973
|
+
if (typeof entry.path === "string" && typeof entry.relativePath === "string" && (entry.status === "added" || entry.status === "removed" || entry.status === "modified")) {
|
|
36974
|
+
changes.push({ path: entry.path, relativePath: entry.relativePath, status: entry.status });
|
|
36825
36975
|
}
|
|
36826
|
-
continue;
|
|
36827
36976
|
}
|
|
36828
|
-
if (
|
|
36829
|
-
|
|
36830
|
-
const sourceUris = recordsInGroup.map((record2) => record2.uri);
|
|
36831
|
-
keepUpdates.push({
|
|
36832
|
-
content: memoryContentWithHygieneSources(keep, sourceUris),
|
|
36833
|
-
reason: "keep latest handoff and preserve source URIs",
|
|
36834
|
-
sourceUris,
|
|
36835
|
-
uri: keep.uri
|
|
36836
|
-
});
|
|
36837
|
-
for (const record2 of sortedNewestFirst(remainingRecords).filter((item) => item.uri !== keep.uri)) {
|
|
36838
|
-
archives.push({
|
|
36839
|
-
kind,
|
|
36840
|
-
project,
|
|
36841
|
-
reason: `older handoff for ${project}/${topic ?? "unknown"}`,
|
|
36842
|
-
topic,
|
|
36843
|
-
uri: record2.uri
|
|
36844
|
-
});
|
|
36845
|
-
}
|
|
36846
|
-
continue;
|
|
36847
|
-
}
|
|
36848
|
-
for (const record2 of sortedNewestFirst(remainingRecords)) {
|
|
36849
|
-
manualReview.push({ reason: `non-exact ${kind} memory in overlapping group`, uri: record2.uri });
|
|
36977
|
+
if (changes.length > 0) {
|
|
36978
|
+
pending.set(team, changes);
|
|
36850
36979
|
}
|
|
36851
36980
|
}
|
|
36852
|
-
|
|
36853
|
-
archives: dedupeByUri(archives),
|
|
36854
|
-
forgets: dedupeByUri(forgets),
|
|
36855
|
-
keepUpdates: dedupeByUri(keepUpdates),
|
|
36856
|
-
manualReview: dedupeByUri(manualReview),
|
|
36857
|
-
options,
|
|
36858
|
-
recordsScanned: groupedRecords.length
|
|
36859
|
-
};
|
|
36981
|
+
state.pendingReindexes = pending;
|
|
36860
36982
|
}
|
|
36861
|
-
function
|
|
36862
|
-
const
|
|
36863
|
-
|
|
36864
|
-
|
|
36865
|
-
|
|
36866
|
-
|
|
36983
|
+
async function writePendingReindexes(config2, state) {
|
|
36984
|
+
const path = pendingReindexesPath(config2);
|
|
36985
|
+
if (state.pendingReindexes.size === 0) {
|
|
36986
|
+
await (0, import_promises4.rm)(path, { force: true });
|
|
36987
|
+
return;
|
|
36988
|
+
}
|
|
36989
|
+
const contents = {
|
|
36990
|
+
teams: Object.fromEntries(state.pendingReindexes),
|
|
36991
|
+
version: 1
|
|
36867
36992
|
};
|
|
36868
|
-
|
|
36869
|
-
|
|
36870
|
-
|
|
36871
|
-
|
|
36872
|
-
);
|
|
36993
|
+
await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
|
|
36994
|
+
const tempPath = `${path}.${process.pid}.tmp`;
|
|
36995
|
+
await (0, import_promises4.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
|
|
36996
|
+
`, { encoding: "utf8", mode: 384 });
|
|
36997
|
+
await (0, import_promises4.rename)(tempPath, path);
|
|
36873
36998
|
}
|
|
36874
|
-
function
|
|
36875
|
-
const
|
|
36876
|
-
|
|
36877
|
-
|
|
36878
|
-
|
|
36879
|
-
|
|
36880
|
-
const
|
|
36881
|
-
|
|
36882
|
-
`Records scanned: ${plan.recordsScanned}`,
|
|
36883
|
-
"",
|
|
36884
|
-
formatPlanSection(
|
|
36885
|
-
"Keep/update",
|
|
36886
|
-
plan.keepUpdates.map((action) => `${action.uri} (${action.reason}; sources: ${action.sourceUris.length})`)
|
|
36887
|
-
),
|
|
36888
|
-
formatPlanSection(
|
|
36889
|
-
"Archive old handoffs",
|
|
36890
|
-
plan.archives.map((action) => `${action.uri} (${action.reason})`)
|
|
36891
|
-
),
|
|
36892
|
-
formatPlanSection(
|
|
36893
|
-
"Forget exact duplicates",
|
|
36894
|
-
plan.forgets.map((action) => `${action.uri} (${action.reason})`)
|
|
36895
|
-
),
|
|
36896
|
-
formatPlanSection(
|
|
36897
|
-
"Manual review",
|
|
36898
|
-
plan.manualReview.map((item) => `${item.uri} (${item.reason})`)
|
|
36899
|
-
)
|
|
36900
|
-
];
|
|
36901
|
-
if (!options.apply) {
|
|
36902
|
-
lines.push("", "No changes made. Re-run with --apply to execute this plan.");
|
|
36999
|
+
async function refreshShareUpdateState(config2, options) {
|
|
37000
|
+
const state = autoShareState(config2);
|
|
37001
|
+
const warnings = await enqueueShareOperation(
|
|
37002
|
+
state,
|
|
37003
|
+
async () => refreshShareUpdateStateLocked(config2, state, options)
|
|
37004
|
+
);
|
|
37005
|
+
for (const warning2 of warnings) {
|
|
37006
|
+
console.error(warning2);
|
|
36903
37007
|
}
|
|
36904
|
-
return lines.join("\n");
|
|
36905
37008
|
}
|
|
36906
|
-
function
|
|
36907
|
-
const
|
|
36908
|
-
|
|
36909
|
-
|
|
36910
|
-
const returnedRecords = options.records?.filter((record2) => returnedUriSet.has(record2.uri)).map((record2) => groupableRecord(record2)) ?? [];
|
|
36911
|
-
const groups = /* @__PURE__ */ new Map();
|
|
36912
|
-
for (const item of returnedRecords) {
|
|
36913
|
-
if (!item) {
|
|
36914
|
-
continue;
|
|
36915
|
-
}
|
|
36916
|
-
groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
|
|
37009
|
+
async function refreshShareUpdateStateLocked(config2, state, options) {
|
|
37010
|
+
const now = Date.now();
|
|
37011
|
+
if (!options.force && state.lastCheckedAt > 0 && now - state.lastCheckedAt < AUTO_SHARE_FETCH_INTERVAL_MS) {
|
|
37012
|
+
return [];
|
|
36917
37013
|
}
|
|
36918
|
-
|
|
36919
|
-
|
|
36920
|
-
|
|
36921
|
-
|
|
36922
|
-
|
|
36923
|
-
|
|
36924
|
-
|
|
36925
|
-
|
|
36926
|
-
|
|
36927
|
-
|
|
36928
|
-
|
|
37014
|
+
try {
|
|
37015
|
+
const statuses = await fetchShareUpdateStatuses(config2);
|
|
37016
|
+
const nextBehindTeams = new Set(state.behindTeams);
|
|
37017
|
+
for (const status of statuses) {
|
|
37018
|
+
if (status.warning) {
|
|
37019
|
+
continue;
|
|
37020
|
+
}
|
|
37021
|
+
if (status.behind > 0) {
|
|
37022
|
+
nextBehindTeams.add(status.team);
|
|
37023
|
+
} else {
|
|
37024
|
+
nextBehindTeams.delete(status.team);
|
|
37025
|
+
}
|
|
36929
37026
|
}
|
|
36930
|
-
|
|
36931
|
-
|
|
36932
|
-
|
|
37027
|
+
state.behindTeams = nextBehindTeams;
|
|
37028
|
+
return statuses.flatMap((status) => status.warning ? [status.warning] : []);
|
|
37029
|
+
} finally {
|
|
37030
|
+
state.lastCheckedAt = Date.now();
|
|
36933
37031
|
}
|
|
36934
|
-
|
|
36935
|
-
|
|
36936
|
-
|
|
36937
|
-
|
|
36938
|
-
|
|
36939
|
-
|
|
36940
|
-
|
|
37032
|
+
}
|
|
37033
|
+
function enqueueShareOperation(state, action) {
|
|
37034
|
+
const previous = state.operationPromise ?? Promise.resolve();
|
|
37035
|
+
const current = previous.catch(() => void 0).then(action);
|
|
37036
|
+
state.operationPromise = current.catch(() => void 0);
|
|
37037
|
+
return current;
|
|
37038
|
+
}
|
|
37039
|
+
async function fetchShareUpdateStatuses(config2) {
|
|
37040
|
+
const teamsFile = await readTeamsFile(config2);
|
|
37041
|
+
const teams = Object.entries(teamsFile.teams);
|
|
37042
|
+
if (teams.length === 0) {
|
|
37043
|
+
return [];
|
|
36941
37044
|
}
|
|
36942
|
-
|
|
36943
|
-
|
|
37045
|
+
const git = await requiredExecutable("git");
|
|
37046
|
+
const statuses = [];
|
|
37047
|
+
for (const [name, team] of teams) {
|
|
37048
|
+
const fetchResult = await runCommand(git, ["-C", team.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME], {
|
|
37049
|
+
allowFailure: true
|
|
37050
|
+
});
|
|
37051
|
+
if (fetchResult.exitCode !== 0) {
|
|
37052
|
+
statuses.push({
|
|
37053
|
+
behind: 0,
|
|
37054
|
+
team: name,
|
|
37055
|
+
warning: `Auto-sync check for shared team "${name}" failed: ${fetchResult.stderr.trim() || fetchResult.stdout.trim() || "unknown git fetch error"}`
|
|
37056
|
+
});
|
|
36944
37057
|
continue;
|
|
36945
37058
|
}
|
|
36946
|
-
|
|
36947
|
-
|
|
36948
|
-
|
|
36949
|
-
|
|
36950
|
-
|
|
36951
|
-
}
|
|
36952
|
-
|
|
36953
|
-
const userSegment = uriSegment(user);
|
|
36954
|
-
const matches = text.matchAll(/viking:\/\/[^\s)]+/g);
|
|
36955
|
-
const uris = [];
|
|
36956
|
-
for (const match of matches) {
|
|
36957
|
-
const uri = match[0]?.replace(/[.,;:]+$/g, "");
|
|
36958
|
-
if (!uri || !parsePersonalMemoryUri(uri, userSegment)) {
|
|
37059
|
+
const behind = await gitOutput(team.worktree, ["rev-list", "--count", "HEAD..@{u}"], false);
|
|
37060
|
+
if (behind === void 0) {
|
|
37061
|
+
statuses.push({
|
|
37062
|
+
behind: 0,
|
|
37063
|
+
team: name,
|
|
37064
|
+
warning: `Auto-sync check for shared team "${name}" failed: could not read upstream behind count.`
|
|
37065
|
+
});
|
|
36959
37066
|
continue;
|
|
36960
37067
|
}
|
|
36961
|
-
|
|
37068
|
+
statuses.push({ behind: Number.parseInt(behind, 10) || 0, team: name });
|
|
36962
37069
|
}
|
|
36963
|
-
return
|
|
37070
|
+
return statuses;
|
|
36964
37071
|
}
|
|
36965
|
-
function
|
|
36966
|
-
const
|
|
36967
|
-
|
|
36968
|
-
|
|
37072
|
+
async function runShareSyncQuiet(config2, state, options) {
|
|
37073
|
+
const team = await resolveTeam(config2, options.team);
|
|
37074
|
+
const git = await requiredExecutable("git");
|
|
37075
|
+
const worktree = team.config.worktree;
|
|
37076
|
+
const pendingChanges = state.pendingReindexes.get(team.name);
|
|
37077
|
+
if (pendingChanges && pendingChanges.length > 0) {
|
|
37078
|
+
await applyAndPersistChanges(config2, team.config, state, pendingChanges, { quiet: true });
|
|
36969
37079
|
}
|
|
36970
|
-
|
|
36971
|
-
|
|
36972
|
-
if (parts.length < 4) {
|
|
36973
|
-
return void 0;
|
|
37080
|
+
if (await hasUncommittedChanges(worktree)) {
|
|
37081
|
+
return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
|
|
36974
37082
|
}
|
|
36975
|
-
|
|
36976
|
-
|
|
37083
|
+
const ahead = await gitOutput(worktree, ["rev-list", "--count", "@{u}..HEAD"], false);
|
|
37084
|
+
if (ahead === void 0) {
|
|
37085
|
+
return `Shared team "${team.name}" upstream status is unknown; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to inspect and resolve it.`;
|
|
36977
37086
|
}
|
|
36978
|
-
if (
|
|
36979
|
-
return
|
|
37087
|
+
if ((Number.parseInt(ahead, 10) || 0) > 0) {
|
|
37088
|
+
return `Shared team "${team.name}" has local commits ahead of upstream; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or reconcile them.`;
|
|
36980
37089
|
}
|
|
36981
|
-
|
|
36982
|
-
|
|
37090
|
+
const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
|
|
37091
|
+
const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
|
|
37092
|
+
if (pullResult.exitCode !== 0) {
|
|
37093
|
+
if (await exists((0, import_node_path3.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path3.join)(team.config.gitdir, "rebase-apply"))) {
|
|
37094
|
+
throw new Error(
|
|
37095
|
+
`Automatic share sync hit git conflicts in ${worktree}. Resolve them in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then rerun recall/read.`
|
|
37096
|
+
);
|
|
37097
|
+
}
|
|
37098
|
+
throw new Error(
|
|
37099
|
+
`Automatic share sync failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
|
|
37100
|
+
);
|
|
37101
|
+
}
|
|
37102
|
+
const afterRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
|
|
37103
|
+
if (beforeRev && afterRev && beforeRev !== afterRev) {
|
|
37104
|
+
const changes = await listChangedFiles(worktree, beforeRev, afterRev);
|
|
37105
|
+
if (changes.length > 0) {
|
|
37106
|
+
const stillPending = state.pendingReindexes.get(team.name) ?? [];
|
|
37107
|
+
const combined = mergeChanges(stillPending, changes);
|
|
37108
|
+
await applyAndPersistChanges(config2, team.config, state, combined, { quiet: true });
|
|
37109
|
+
}
|
|
36983
37110
|
}
|
|
36984
37111
|
return void 0;
|
|
36985
37112
|
}
|
|
36986
|
-
function
|
|
36987
|
-
|
|
36988
|
-
|
|
37113
|
+
async function publishShareGitChange(worktree, relativePath, commitMessage, options = {}) {
|
|
37114
|
+
const dryRun = options.dryRun === true;
|
|
37115
|
+
const push = options.push !== false;
|
|
37116
|
+
const verb = options.verb ?? "add";
|
|
37117
|
+
const git = await requiredExecutable("git");
|
|
37118
|
+
const messages = [];
|
|
37119
|
+
const paths = typeof relativePath === "string" ? [relativePath] : [...relativePath];
|
|
37120
|
+
const stageArgs = verb === "rm" ? ["-C", worktree, "rm", ...paths] : ["-C", worktree, "add", "--", ...paths];
|
|
37121
|
+
const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
|
|
37122
|
+
if (stageResult) {
|
|
37123
|
+
messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
|
|
36989
37124
|
}
|
|
36990
|
-
|
|
36991
|
-
|
|
36992
|
-
|
|
37125
|
+
if (dryRun) {
|
|
37126
|
+
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "commit", "-m", commitMessage])}`);
|
|
37127
|
+
} else {
|
|
37128
|
+
const commitResult = await runCommand(git, ["-C", worktree, "commit", "-m", commitMessage], { allowFailure: true });
|
|
37129
|
+
if (commitResult.exitCode !== 0) {
|
|
37130
|
+
const detail = commitResult.stdout.trim() || commitResult.stderr.trim();
|
|
37131
|
+
if (/nothing to commit|no changes added/i.test(detail)) {
|
|
37132
|
+
messages.push("git commit: nothing to commit (file already in tree)");
|
|
37133
|
+
} else {
|
|
37134
|
+
throw new Error(`git commit failed: ${detail || "unknown error"}`);
|
|
37135
|
+
}
|
|
37136
|
+
} else {
|
|
37137
|
+
messages.push(`git commit: ${commitResult.stdout.trim().split("\n").slice(0, 2).join(" ")}`);
|
|
37138
|
+
}
|
|
36993
37139
|
}
|
|
36994
|
-
|
|
36995
|
-
|
|
36996
|
-
|
|
36997
|
-
}
|
|
36998
|
-
|
|
36999
|
-
|
|
37140
|
+
if (!push) {
|
|
37141
|
+
messages.push("git push skipped (push=false)");
|
|
37142
|
+
return messages;
|
|
37143
|
+
}
|
|
37144
|
+
const pushResult = await runGitCommand(
|
|
37145
|
+
dryRun,
|
|
37146
|
+
git,
|
|
37147
|
+
["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME],
|
|
37148
|
+
"git push failed"
|
|
37149
|
+
);
|
|
37150
|
+
if (pushResult) {
|
|
37151
|
+
messages.push(`git push: ${pushResult.stdout.trim() || pushResult.stderr.trim() || "ok"}`);
|
|
37152
|
+
}
|
|
37153
|
+
return messages;
|
|
37000
37154
|
}
|
|
37001
|
-
function
|
|
37002
|
-
|
|
37003
|
-
|
|
37155
|
+
async function runGitCommand(dryRun, git, args, failureLabel) {
|
|
37156
|
+
if (dryRun) {
|
|
37157
|
+
console.log(`Would run: ${formatShellCommand(git, args)}`);
|
|
37158
|
+
return void 0;
|
|
37159
|
+
}
|
|
37160
|
+
const result = await runCommand(git, args, { allowFailure: true });
|
|
37161
|
+
if (result.exitCode !== 0) {
|
|
37162
|
+
throw new Error(`${failureLabel}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`);
|
|
37163
|
+
}
|
|
37164
|
+
return result;
|
|
37004
37165
|
}
|
|
37005
|
-
function
|
|
37006
|
-
const
|
|
37007
|
-
|
|
37166
|
+
async function shareAgentArtifact(config2, sourcePath, options) {
|
|
37167
|
+
const team = await resolveTeam(config2, options.team);
|
|
37168
|
+
const resolvedSourcePath = expandPath(sourcePath);
|
|
37169
|
+
if (!await isRegularFileNoSymlink(resolvedSourcePath)) {
|
|
37170
|
+
throw new Error(`Agent artifact source is not a regular file: ${resolvedSourcePath}`);
|
|
37171
|
+
}
|
|
37172
|
+
const artifact = inferShareArtifact(resolvedSourcePath, options);
|
|
37173
|
+
if (artifact.kind === "skill") {
|
|
37174
|
+
const skillDir = (0, import_node_path3.dirname)(resolvedSourcePath);
|
|
37175
|
+
const members = await collectBundleMemberFiles(skillDir);
|
|
37176
|
+
if (members.length > 1) {
|
|
37177
|
+
return shareBundleArtifact(config2, team, artifact, skillDir, members, options);
|
|
37178
|
+
}
|
|
37179
|
+
}
|
|
37180
|
+
return shareSingleArtifact(config2, team, resolvedSourcePath, artifact, options);
|
|
37008
37181
|
}
|
|
37009
|
-
function
|
|
37010
|
-
const
|
|
37011
|
-
|
|
37012
|
-
|
|
37182
|
+
async function shareSingleArtifact(config2, team, resolvedSourcePath, artifact, options) {
|
|
37183
|
+
const dryRun = options.dryRun === true;
|
|
37184
|
+
const preview = options.preview === true;
|
|
37185
|
+
const rawContent = await (0, import_promises4.readFile)(resolvedSourcePath, "utf8");
|
|
37186
|
+
if (!rawContent.trim()) {
|
|
37187
|
+
throw new Error(`Refusing to share empty agent artifact: ${resolvedSourcePath}`);
|
|
37013
37188
|
}
|
|
37014
|
-
|
|
37015
|
-
|
|
37189
|
+
const scrub = applyScrubber(rawContent, { redact: options.redact === true });
|
|
37190
|
+
const relativePath = sharedArtifactRelativePath(artifact);
|
|
37191
|
+
const targetPath = (0, import_node_path3.join)(team.config.worktree, ...relativePath.split("/"));
|
|
37192
|
+
const targetUri = workfileToVikingUri(config2, team.config, targetPath);
|
|
37193
|
+
const messages = [
|
|
37194
|
+
`${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} ${artifact.kind} ${artifact.agent}/${artifact.name}`,
|
|
37195
|
+
`Source: ${portablePath(resolvedSourcePath)}`,
|
|
37196
|
+
`Destination: ${targetUri}`
|
|
37197
|
+
];
|
|
37198
|
+
if (preview) {
|
|
37199
|
+
if (scrub.blocker) {
|
|
37200
|
+
messages.push(`PREVIEW BLOCKED: ${scrub.blocker}. Strip the sensitive value or pass --redact.`);
|
|
37201
|
+
return {
|
|
37202
|
+
artifact,
|
|
37203
|
+
gitMessages: [],
|
|
37204
|
+
messages,
|
|
37205
|
+
sourcePath: resolvedSourcePath,
|
|
37206
|
+
targetPath,
|
|
37207
|
+
targetUri
|
|
37208
|
+
};
|
|
37209
|
+
}
|
|
37210
|
+
for (const redaction of scrub.redactions) {
|
|
37211
|
+
messages.push(`PREVIEW redact: ${redaction.count}\xD7 ${redaction.name}`);
|
|
37212
|
+
}
|
|
37213
|
+
return {
|
|
37214
|
+
artifact,
|
|
37215
|
+
gitMessages: [],
|
|
37216
|
+
messages,
|
|
37217
|
+
previewContent: scrub.cleaned,
|
|
37218
|
+
sourcePath: resolvedSourcePath,
|
|
37219
|
+
targetPath,
|
|
37220
|
+
targetUri
|
|
37221
|
+
};
|
|
37016
37222
|
}
|
|
37017
|
-
|
|
37223
|
+
if (scrub.blocker) {
|
|
37224
|
+
throw new Error(
|
|
37225
|
+
`Refusing to share ${resolvedSourcePath}: possible ${scrub.blocker}. Strip the sensitive value or pass --redact for soft-leak patterns.`
|
|
37226
|
+
);
|
|
37227
|
+
}
|
|
37228
|
+
for (const redaction of scrub.redactions) {
|
|
37229
|
+
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before sharing.`);
|
|
37230
|
+
}
|
|
37231
|
+
const content = scrub.cleaned;
|
|
37232
|
+
const existingContent = await readFileIfExists(targetPath) ?? void 0;
|
|
37233
|
+
if (existingContent !== void 0 && existingContent !== content && options.force !== true) {
|
|
37234
|
+
throw new Error(
|
|
37235
|
+
`Shared artifact already exists with different content: ${portablePath(targetPath)}. Pass --force to replace it.`
|
|
37236
|
+
);
|
|
37237
|
+
}
|
|
37238
|
+
if (dryRun) {
|
|
37239
|
+
messages.push(`Would write shared artifact: ${portablePath(targetPath)}`);
|
|
37240
|
+
}
|
|
37241
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
37242
|
+
const ovHasResource = !dryRun && await vikingResourceExists(ov, config2, targetUri);
|
|
37243
|
+
await ensureSharedDirectoryChain(config2, ov, targetUri, dryRun, { quiet: true });
|
|
37244
|
+
await writeMemoryFile(config2, ov, targetUri, content, ovHasResource ? "replace" : "create", dryRun, { quiet: true });
|
|
37245
|
+
const message = options.message ?? `share: publish ${relativePath}`;
|
|
37246
|
+
const gitMessages = await publishShareGitChange(team.config.worktree, relativePath, message, {
|
|
37247
|
+
dryRun,
|
|
37248
|
+
push: options.push
|
|
37249
|
+
});
|
|
37250
|
+
return { artifact, gitMessages, messages, sourcePath: resolvedSourcePath, targetPath, targetUri };
|
|
37018
37251
|
}
|
|
37019
|
-
function
|
|
37020
|
-
|
|
37252
|
+
async function shareBundleArtifact(config2, team, artifact, skillDir, members, options) {
|
|
37253
|
+
const dryRun = options.dryRun === true;
|
|
37254
|
+
const preview = options.preview === true;
|
|
37255
|
+
const skillRootRelative = `${SHAREABLE_ARTIFACT_DIR}/skills/${artifact.agent}/${artifact.name}`;
|
|
37256
|
+
const skillRootTargetDir = (0, import_node_path3.join)(team.config.worktree, ...skillRootRelative.split("/"));
|
|
37257
|
+
const skillMdTargetPath = (0, import_node_path3.join)(skillRootTargetDir, "SKILL.md");
|
|
37258
|
+
const skillMdTargetUri = workfileToVikingUri(config2, team.config, skillMdTargetPath);
|
|
37259
|
+
const skillRootTargetUri = parentUri(skillMdTargetUri);
|
|
37260
|
+
const skillMdSourcePath = (0, import_node_path3.join)(skillDir, "SKILL.md");
|
|
37261
|
+
const prepared = await Promise.all(
|
|
37262
|
+
members.map((member) => prepareBundleMember(config2, team, member, skillRootTargetDir, options))
|
|
37263
|
+
);
|
|
37264
|
+
const skillMd = prepared.find((entry) => entry.relativePath === "SKILL.md");
|
|
37265
|
+
if (skillMd === void 0) {
|
|
37266
|
+
throw new Error(`Skill bundle ${artifact.agent}/${artifact.name} is missing SKILL.md.`);
|
|
37267
|
+
}
|
|
37268
|
+
if (!skillMd.binary && typeof skillMd.content === "string" && !skillMd.content.trim()) {
|
|
37269
|
+
throw new Error(`Refusing to share empty agent artifact: ${skillMdSourcePath}`);
|
|
37270
|
+
}
|
|
37271
|
+
const messages = [
|
|
37272
|
+
`${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} skill ${artifact.agent}/${artifact.name} bundle (${prepared.length} files)`,
|
|
37273
|
+
`Source: ${portablePath(skillDir)}`,
|
|
37274
|
+
`Destination: ${skillRootTargetUri}/`
|
|
37275
|
+
];
|
|
37276
|
+
const blockers = prepared.filter((entry) => entry.blocker !== void 0);
|
|
37277
|
+
if (preview) {
|
|
37278
|
+
for (const entry of prepared) {
|
|
37279
|
+
const flags = entry.binary ? ["binary"] : [];
|
|
37280
|
+
for (const redaction of entry.redactions) {
|
|
37281
|
+
flags.push(`redact ${redaction.count}\xD7 ${redaction.name}`);
|
|
37282
|
+
}
|
|
37283
|
+
const note = entry.blocker !== void 0 ? ` BLOCKED: ${entry.blocker}` : "";
|
|
37284
|
+
messages.push(` ${entry.relativePath}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}${note}`);
|
|
37285
|
+
}
|
|
37286
|
+
return {
|
|
37287
|
+
artifact,
|
|
37288
|
+
gitMessages: [],
|
|
37289
|
+
messages,
|
|
37290
|
+
previewContent: skillMd.binary ? void 0 : skillMd.content,
|
|
37291
|
+
sourcePath: skillMdSourcePath,
|
|
37292
|
+
targetPath: skillMdTargetPath,
|
|
37293
|
+
targetUri: skillMdTargetUri
|
|
37294
|
+
};
|
|
37295
|
+
}
|
|
37296
|
+
if (blockers.length > 0) {
|
|
37297
|
+
throw new Error(
|
|
37298
|
+
`Refusing to share skill ${artifact.agent}/${artifact.name}: ${blockers.map((entry) => `${entry.relativePath} (${entry.blocker})`).join("; ")}. Strip the value, pass --redact for local paths, or --allow-binary for binary files.`
|
|
37299
|
+
);
|
|
37300
|
+
}
|
|
37301
|
+
for (const entry of prepared) {
|
|
37302
|
+
for (const redaction of entry.redactions) {
|
|
37303
|
+
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} in ${entry.relativePath} before sharing.`);
|
|
37304
|
+
}
|
|
37305
|
+
}
|
|
37306
|
+
for (const entry of prepared) {
|
|
37307
|
+
const existing = await readFileBytesIfExists(entry.targetPath);
|
|
37308
|
+
if (existing !== void 0 && sha256(existing) !== entry.sha256 && options.force !== true) {
|
|
37309
|
+
throw new Error(
|
|
37310
|
+
`Shared artifact already exists with different content: ${portablePath(entry.targetPath)}. Pass --force to replace it.`
|
|
37311
|
+
);
|
|
37312
|
+
}
|
|
37313
|
+
}
|
|
37314
|
+
if (dryRun) {
|
|
37315
|
+
messages.push(`Would write ${prepared.length} files under ${portablePath(skillRootTargetDir)}`);
|
|
37316
|
+
return {
|
|
37317
|
+
artifact,
|
|
37318
|
+
gitMessages: [],
|
|
37319
|
+
messages,
|
|
37320
|
+
sourcePath: skillMdSourcePath,
|
|
37321
|
+
targetPath: skillMdTargetPath,
|
|
37322
|
+
targetUri: skillMdTargetUri
|
|
37323
|
+
};
|
|
37324
|
+
}
|
|
37325
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
37326
|
+
const markdownMembers = orderSkillMdFirst(prepared.filter((entry) => entry.relativePath.endsWith(".md")));
|
|
37327
|
+
const otherMembers = prepared.filter((entry) => !entry.relativePath.endsWith(".md"));
|
|
37328
|
+
for (const entry of markdownMembers) {
|
|
37329
|
+
const ovHasResource = await vikingResourceExists(ov, config2, entry.targetUri);
|
|
37330
|
+
await ensureSharedDirectoryChain(config2, ov, entry.targetUri, dryRun, { quiet: true });
|
|
37331
|
+
await writeMemoryFile(
|
|
37332
|
+
config2,
|
|
37333
|
+
ov,
|
|
37334
|
+
entry.targetUri,
|
|
37335
|
+
entry.content,
|
|
37336
|
+
ovHasResource ? "replace" : "create",
|
|
37337
|
+
dryRun,
|
|
37338
|
+
{ quiet: true }
|
|
37339
|
+
);
|
|
37340
|
+
}
|
|
37341
|
+
await ensureDirectory(skillRootTargetDir, false);
|
|
37342
|
+
for (const entry of otherMembers) {
|
|
37343
|
+
await ensureDirectory((0, import_node_path3.dirname)(entry.targetPath), false);
|
|
37344
|
+
await (0, import_promises4.writeFile)(entry.targetPath, entry.content, entry.binary ? { mode: 384 } : { encoding: "utf8", mode: 384 });
|
|
37345
|
+
}
|
|
37346
|
+
await (0, import_promises4.writeFile)((0, import_node_path3.join)(skillRootTargetDir, BUNDLE_MANIFEST_FILE), buildBundleManifest(artifact, prepared), {
|
|
37347
|
+
encoding: "utf8",
|
|
37348
|
+
mode: 384
|
|
37349
|
+
});
|
|
37350
|
+
const stagedPaths = [
|
|
37351
|
+
...prepared.map((entry) => `${skillRootRelative}/${entry.relativePath}`),
|
|
37352
|
+
`${skillRootRelative}/${BUNDLE_MANIFEST_FILE}`
|
|
37353
|
+
];
|
|
37354
|
+
const message = options.message ?? `share: publish skill ${artifact.agent}/${artifact.name} (${prepared.length} files)`;
|
|
37355
|
+
const gitMessages = await publishShareGitChange(team.config.worktree, stagedPaths, message, {
|
|
37356
|
+
dryRun,
|
|
37357
|
+
push: options.push
|
|
37358
|
+
});
|
|
37359
|
+
return {
|
|
37360
|
+
artifact,
|
|
37361
|
+
gitMessages,
|
|
37362
|
+
messages,
|
|
37363
|
+
sourcePath: skillMdSourcePath,
|
|
37364
|
+
targetPath: skillMdTargetPath,
|
|
37365
|
+
targetUri: skillMdTargetUri
|
|
37366
|
+
};
|
|
37021
37367
|
}
|
|
37022
|
-
function
|
|
37023
|
-
const
|
|
37024
|
-
|
|
37025
|
-
|
|
37026
|
-
|
|
37368
|
+
async function prepareBundleMember(config2, team, member, skillRootTargetDir, options) {
|
|
37369
|
+
const buffer = await (0, import_promises4.readFile)(member.absolutePath);
|
|
37370
|
+
const targetPath = (0, import_node_path3.join)(skillRootTargetDir, ...member.relativePath.split("/"));
|
|
37371
|
+
const targetUri = workfileToVikingUri(config2, team.config, targetPath);
|
|
37372
|
+
if (isProbablyBinary(buffer)) {
|
|
37373
|
+
const credential = detectBinaryCredential(buffer);
|
|
37374
|
+
const blocker = credential !== void 0 ? `possible ${credential} embedded in binary file` : options.allowBinary === true ? void 0 : "binary file (pass --allow-binary to include it unscanned)";
|
|
37375
|
+
return {
|
|
37376
|
+
binary: true,
|
|
37377
|
+
blocker,
|
|
37378
|
+
content: buffer,
|
|
37379
|
+
redactions: [],
|
|
37380
|
+
relativePath: member.relativePath,
|
|
37381
|
+
sha256: sha256(buffer),
|
|
37382
|
+
targetPath,
|
|
37383
|
+
targetUri
|
|
37384
|
+
};
|
|
37027
37385
|
}
|
|
37028
|
-
|
|
37386
|
+
const scrub = applyScrubber(buffer.toString("utf8"), { redact: options.redact === true });
|
|
37387
|
+
return {
|
|
37388
|
+
binary: false,
|
|
37389
|
+
blocker: scrub.blocker,
|
|
37390
|
+
content: scrub.cleaned,
|
|
37391
|
+
redactions: scrub.redactions,
|
|
37392
|
+
relativePath: member.relativePath,
|
|
37393
|
+
sha256: sha256(scrub.cleaned),
|
|
37394
|
+
targetPath,
|
|
37395
|
+
targetUri
|
|
37396
|
+
};
|
|
37029
37397
|
}
|
|
37030
|
-
function
|
|
37031
|
-
|
|
37032
|
-
|
|
37398
|
+
function orderSkillMdFirst(entries) {
|
|
37399
|
+
return [...entries].sort((a, b) => {
|
|
37400
|
+
if (a.relativePath === "SKILL.md") {
|
|
37401
|
+
return -1;
|
|
37402
|
+
}
|
|
37403
|
+
if (b.relativePath === "SKILL.md") {
|
|
37404
|
+
return 1;
|
|
37405
|
+
}
|
|
37406
|
+
return compareStrings(a.relativePath, b.relativePath);
|
|
37407
|
+
});
|
|
37033
37408
|
}
|
|
37034
|
-
function
|
|
37035
|
-
const
|
|
37036
|
-
|
|
37409
|
+
function buildBundleManifest(artifact, prepared) {
|
|
37410
|
+
const manifest = {
|
|
37411
|
+
artifact,
|
|
37412
|
+
members: prepared.map((entry) => ({ binary: entry.binary, path: entry.relativePath, sha256: entry.sha256 })).sort((a, b) => compareStrings(a.path, b.path)),
|
|
37413
|
+
version: BUNDLE_MANIFEST_VERSION
|
|
37414
|
+
};
|
|
37415
|
+
return `${JSON.stringify(manifest, void 0, 2)}
|
|
37416
|
+
`;
|
|
37037
37417
|
}
|
|
37038
|
-
function
|
|
37039
|
-
|
|
37040
|
-
|
|
37041
|
-
|
|
37042
|
-
|
|
37418
|
+
async function collectBundleMemberFiles(skillDir) {
|
|
37419
|
+
const out = [];
|
|
37420
|
+
async function visit(dir) {
|
|
37421
|
+
const entries = await (0, import_promises4.readdir)(dir, { withFileTypes: true });
|
|
37422
|
+
for (const entry of entries) {
|
|
37423
|
+
if (entry.isSymbolicLink()) {
|
|
37424
|
+
continue;
|
|
37425
|
+
}
|
|
37426
|
+
const full = (0, import_node_path3.join)(dir, entry.name);
|
|
37427
|
+
if (entry.isDirectory()) {
|
|
37428
|
+
if (!BUNDLE_IGNORE_DIR_NAMES.includes(entry.name)) {
|
|
37429
|
+
await visit(full);
|
|
37430
|
+
}
|
|
37431
|
+
continue;
|
|
37432
|
+
}
|
|
37433
|
+
if (!entry.isFile() || isIgnoredBundleFile(entry.name)) {
|
|
37434
|
+
continue;
|
|
37435
|
+
}
|
|
37436
|
+
out.push({ absolutePath: full, relativePath: (0, import_node_path3.relative)(skillDir, full).split(import_node_path3.sep).join("/") });
|
|
37437
|
+
}
|
|
37438
|
+
}
|
|
37439
|
+
await visit(skillDir);
|
|
37440
|
+
return out.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
37043
37441
|
}
|
|
37044
|
-
function
|
|
37045
|
-
|
|
37046
|
-
|
|
37442
|
+
function isIgnoredBundleFile(name) {
|
|
37443
|
+
if (name === ".DS_Store" || name === BUNDLE_MANIFEST_FILE || name === BUNDLE_INSTALL_METADATA_FILE) {
|
|
37444
|
+
return true;
|
|
37445
|
+
}
|
|
37446
|
+
if (OV_SUMMARY_FILES.includes(name)) {
|
|
37447
|
+
return true;
|
|
37448
|
+
}
|
|
37449
|
+
return name.endsWith(".log") || name.endsWith(".threadnote-install.json");
|
|
37047
37450
|
}
|
|
37048
|
-
function
|
|
37049
|
-
if (
|
|
37050
|
-
return
|
|
37451
|
+
function isProbablyBinary(buffer) {
|
|
37452
|
+
if (buffer.includes(0)) {
|
|
37453
|
+
return true;
|
|
37051
37454
|
}
|
|
37052
|
-
|
|
37455
|
+
try {
|
|
37456
|
+
new import_node_util.TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
37053
37457
|
return false;
|
|
37458
|
+
} catch (_err) {
|
|
37459
|
+
return true;
|
|
37054
37460
|
}
|
|
37055
|
-
return /\b(?:PR|pull request)\s+(?:OPEN|open|is open)|awaiting review|waiting for review|next steps?:\s*address PR review/i.test(
|
|
37056
|
-
record2.body
|
|
37057
|
-
);
|
|
37058
37461
|
}
|
|
37059
|
-
function
|
|
37060
|
-
const
|
|
37061
|
-
for (const
|
|
37062
|
-
|
|
37063
|
-
|
|
37462
|
+
function detectBinaryCredential(buffer) {
|
|
37463
|
+
const latin1 = buffer.toString("latin1");
|
|
37464
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
37465
|
+
if (pattern.placeholder === void 0 && pattern.regex.test(latin1)) {
|
|
37466
|
+
return pattern.name;
|
|
37467
|
+
}
|
|
37064
37468
|
}
|
|
37065
|
-
return
|
|
37066
|
-
}
|
|
37067
|
-
function compareGroupedRecordLists(left, right) {
|
|
37068
|
-
return (left[0]?.groupKey ?? "").localeCompare(right[0]?.groupKey ?? "");
|
|
37469
|
+
return void 0;
|
|
37069
37470
|
}
|
|
37070
|
-
function
|
|
37071
|
-
const
|
|
37072
|
-
const
|
|
37073
|
-
|
|
37074
|
-
|
|
37075
|
-
continue;
|
|
37471
|
+
function detectBinaryLocalPath(buffer, rewriteRoots) {
|
|
37472
|
+
const latin1 = buffer.toString("latin1");
|
|
37473
|
+
for (const root of rewriteRoots) {
|
|
37474
|
+
if (root.length > 0 && latin1.includes(root)) {
|
|
37475
|
+
return "machine-local path";
|
|
37076
37476
|
}
|
|
37077
|
-
seen.add(item.uri);
|
|
37078
|
-
result.push(item);
|
|
37079
37477
|
}
|
|
37080
|
-
|
|
37081
|
-
|
|
37082
|
-
|
|
37083
|
-
|
|
37084
|
-
title,
|
|
37085
|
-
`kind: ${metadata.kind}`,
|
|
37086
|
-
`status: ${metadata.status}`,
|
|
37087
|
-
metadata.project ? `project: ${metadata.project}` : void 0,
|
|
37088
|
-
metadata.topic ? `topic: ${metadata.topic}` : void 0,
|
|
37089
|
-
`source_agent_client: ${metadata.sourceAgentClient}`,
|
|
37090
|
-
`timestamp: ${metadata.timestamp}`,
|
|
37091
|
-
metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
|
|
37092
|
-
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
|
|
37093
|
-
].filter((line) => line !== void 0);
|
|
37094
|
-
return [...header, "", body.trim()].join("\n");
|
|
37095
|
-
}
|
|
37096
|
-
function headerValue(header, key) {
|
|
37097
|
-
const prefix = `${key}:`;
|
|
37098
|
-
return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
|
|
37099
|
-
}
|
|
37100
|
-
function parseOptionalMemoryKind(value) {
|
|
37101
|
-
if (!value) {
|
|
37102
|
-
return void 0;
|
|
37103
|
-
}
|
|
37104
|
-
if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
|
|
37105
|
-
return value;
|
|
37478
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
37479
|
+
if (pattern.placeholder !== void 0 && pattern.regex.test(latin1)) {
|
|
37480
|
+
return pattern.name;
|
|
37481
|
+
}
|
|
37106
37482
|
}
|
|
37107
37483
|
return void 0;
|
|
37108
37484
|
}
|
|
37109
|
-
function
|
|
37110
|
-
|
|
37485
|
+
async function readFileBytesIfExists(path) {
|
|
37486
|
+
try {
|
|
37487
|
+
return await (0, import_promises4.readFile)(path);
|
|
37488
|
+
} catch (_err) {
|
|
37111
37489
|
return void 0;
|
|
37112
37490
|
}
|
|
37113
|
-
if (["active", "archived", "superseded"].includes(value)) {
|
|
37114
|
-
return value;
|
|
37115
|
-
}
|
|
37116
|
-
return void 0;
|
|
37117
37491
|
}
|
|
37118
|
-
function
|
|
37119
|
-
|
|
37120
|
-
|
|
37492
|
+
function isBundleArtifact(artifact) {
|
|
37493
|
+
if (artifact.artifact.kind === "pack") {
|
|
37494
|
+
return true;
|
|
37495
|
+
}
|
|
37496
|
+
return artifact.members !== void 0 && artifact.members.length > 1;
|
|
37121
37497
|
}
|
|
37122
|
-
function
|
|
37123
|
-
return
|
|
37498
|
+
function compareStrings(a, b) {
|
|
37499
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
37124
37500
|
}
|
|
37125
|
-
function
|
|
37126
|
-
|
|
37127
|
-
|
|
37128
|
-
|
|
37129
|
-
case "incident":
|
|
37130
|
-
return "incidents";
|
|
37131
|
-
case "durable":
|
|
37132
|
-
return "durable memories";
|
|
37133
|
-
case "preference":
|
|
37134
|
-
return "preferences";
|
|
37135
|
-
case "smoke":
|
|
37136
|
-
return "smoke memories";
|
|
37501
|
+
function parsePackManifest(raw, manifestPath) {
|
|
37502
|
+
const parsed = parseJsonConfigObject(raw);
|
|
37503
|
+
if (parsed === void 0) {
|
|
37504
|
+
throw new Error(`Invalid pack manifest (not a JSON object): ${manifestPath}`);
|
|
37137
37505
|
}
|
|
37138
|
-
|
|
37139
|
-
|
|
37140
|
-
|
|
37141
|
-
return `${title}:
|
|
37142
|
-
- none`;
|
|
37506
|
+
const name = parsed.name;
|
|
37507
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
37508
|
+
throw new Error(`Pack manifest must set a non-empty "name": ${manifestPath}`);
|
|
37143
37509
|
}
|
|
37144
|
-
|
|
37145
|
-
|
|
37146
|
-
|
|
37147
|
-
// src/share.ts
|
|
37148
|
-
var import_promises4 = require("node:fs/promises");
|
|
37149
|
-
var import_node_os2 = require("node:os");
|
|
37150
|
-
var import_node_path4 = require("node:path");
|
|
37151
|
-
var TEAMS_FILE_VERSION = 1;
|
|
37152
|
-
var SHARED_SEGMENT = "shared";
|
|
37153
|
-
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
37154
|
-
var SHAREABLE_ARTIFACT_DIR = "agent-artifacts";
|
|
37155
|
-
var SHAREABLE_TOP_LEVEL_DIRS = [...SHAREABLE_MEMORY_KIND_DIRS, SHAREABLE_ARTIFACT_DIR];
|
|
37156
|
-
var ARTIFACT_INSTALL_METADATA_VERSION = 1;
|
|
37157
|
-
var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
37158
|
-
var DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
37159
|
-
var SCRUBBER_PATTERNS = [
|
|
37160
|
-
// Credentials: never redactable. Blocking is the only safe response —
|
|
37161
|
-
// automated redaction risks false negatives that leave material in git.
|
|
37162
|
-
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
37163
|
-
{ name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
37164
|
-
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
37165
|
-
{ name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
|
|
37166
|
-
{ name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
|
|
37167
|
-
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
37168
|
-
// Matches bare JWTs (three base64url segments). May surface a JWE token in
|
|
37169
|
-
// legitimate docs; if that becomes noisy we can switch to warn-only.
|
|
37170
|
-
{ name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
37171
|
-
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
37172
|
-
// Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
|
|
37173
|
-
// xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
|
|
37174
|
-
{ name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i },
|
|
37175
|
-
// Soft leaks: block by default (so the agent sees them and decides), but
|
|
37176
|
-
// allow opt-in redaction so curated memories with incidental matches can
|
|
37177
|
-
// ship without a manual rewrite. Local home paths are the recurring
|
|
37178
|
-
// real-world leak; the regexes greedily consume the whole path segment
|
|
37179
|
-
// (including subdirectories) up to whitespace or common closing punctuation
|
|
37180
|
-
// so redaction collapses an entire path to a single placeholder rather than
|
|
37181
|
-
// leaving the subpath visible.
|
|
37182
|
-
{ name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
|
|
37183
|
-
{ name: "linux home path", placeholder: "<local-path>", regex: /\b\/home\/[^\s)>"'`,]+/ }
|
|
37184
|
-
];
|
|
37185
|
-
function applyScrubber(content, { redact }) {
|
|
37186
|
-
let cleaned = content;
|
|
37187
|
-
const redactions = [];
|
|
37188
|
-
for (const pattern of SCRUBBER_PATTERNS) {
|
|
37189
|
-
if (!pattern.regex.test(cleaned)) {
|
|
37190
|
-
continue;
|
|
37191
|
-
}
|
|
37192
|
-
if (!pattern.placeholder || !redact) {
|
|
37193
|
-
return { blocker: pattern.name, cleaned: content, redactions: [] };
|
|
37194
|
-
}
|
|
37195
|
-
const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`;
|
|
37196
|
-
const globalRegex = new RegExp(pattern.regex.source, flags);
|
|
37197
|
-
const matches = cleaned.match(globalRegex) ?? [];
|
|
37198
|
-
cleaned = cleaned.replace(globalRegex, pattern.placeholder);
|
|
37199
|
-
redactions.push({ count: matches.length, name: pattern.name });
|
|
37510
|
+
const agent = parsed.agent;
|
|
37511
|
+
if (agent !== "codex" && agent !== "claude") {
|
|
37512
|
+
throw new Error(`Pack manifest "agent" must be "codex" or "claude": ${manifestPath}`);
|
|
37200
37513
|
}
|
|
37201
|
-
|
|
37202
|
-
|
|
37203
|
-
|
|
37204
|
-
|
|
37205
|
-
const state = autoShareState(config2);
|
|
37206
|
-
if (state.timer) {
|
|
37207
|
-
return;
|
|
37514
|
+
const stringArray = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
37515
|
+
const skills = stringArray(parsed.skills);
|
|
37516
|
+
if (skills.length === 0) {
|
|
37517
|
+
throw new Error(`Pack manifest must list at least one skill in "skills": ${manifestPath}`);
|
|
37208
37518
|
}
|
|
37209
|
-
|
|
37210
|
-
|
|
37211
|
-
|
|
37212
|
-
|
|
37213
|
-
|
|
37214
|
-
|
|
37215
|
-
|
|
37216
|
-
|
|
37217
|
-
|
|
37519
|
+
const depsValue = parsed.deps ?? {};
|
|
37520
|
+
const pathRewrites = Array.isArray(parsed.pathRewrites) ? parsed.pathRewrites.map((entry) => typeof entry === "string" ? entry : entry?.from).filter((item) => typeof item === "string").map((rewrite) => rewrite.replace(/\/+$/, "")) : [];
|
|
37521
|
+
for (const rewrite of pathRewrites) {
|
|
37522
|
+
if (!(0, import_node_path3.isAbsolute)(rewrite) || rewrite.split("/").filter(Boolean).length < 2) {
|
|
37523
|
+
throw new Error(
|
|
37524
|
+
`Pack manifest pathRewrites entry must be an absolute repo-root path (got "${rewrite}"): ${manifestPath}`
|
|
37525
|
+
);
|
|
37526
|
+
}
|
|
37527
|
+
}
|
|
37528
|
+
return {
|
|
37529
|
+
agent,
|
|
37530
|
+
deps: {
|
|
37531
|
+
cli: stringArray(depsValue.cli),
|
|
37532
|
+
mcp: stringArray(depsValue.mcp),
|
|
37533
|
+
os: stringArray(depsValue.os),
|
|
37534
|
+
runtime: stringArray(depsValue.runtime)
|
|
37535
|
+
},
|
|
37536
|
+
description: typeof parsed.description === "string" ? parsed.description : void 0,
|
|
37537
|
+
include: stringArray(parsed.include),
|
|
37538
|
+
name,
|
|
37539
|
+
pathRewrites,
|
|
37540
|
+
skills
|
|
37541
|
+
};
|
|
37218
37542
|
}
|
|
37219
|
-
async function
|
|
37220
|
-
const
|
|
37221
|
-
|
|
37222
|
-
|
|
37223
|
-
|
|
37224
|
-
|
|
37225
|
-
if (syncTeams.size === 0) {
|
|
37226
|
-
return { syncedTeams: [], warnings };
|
|
37543
|
+
async function collectPackMembers(manifestDir, manifest) {
|
|
37544
|
+
const members = /* @__PURE__ */ new Map();
|
|
37545
|
+
const addEntry = async (entry) => {
|
|
37546
|
+
const normalized = entry.split("/").filter(Boolean).join("/");
|
|
37547
|
+
if (normalized.split("/").includes("..")) {
|
|
37548
|
+
throw new Error(`Pack manifest entries must stay within the pack root (got "${entry}").`);
|
|
37227
37549
|
}
|
|
37228
|
-
const
|
|
37229
|
-
|
|
37230
|
-
|
|
37231
|
-
|
|
37232
|
-
|
|
37233
|
-
|
|
37234
|
-
|
|
37235
|
-
|
|
37236
|
-
remainingBehind.delete(team);
|
|
37237
|
-
syncedTeams.push(team);
|
|
37238
|
-
}
|
|
37239
|
-
} catch (err) {
|
|
37240
|
-
warnings.push(
|
|
37241
|
-
`Auto-sync for shared team "${team}" failed: ${err instanceof Error ? err.message : String(err)}`
|
|
37242
|
-
);
|
|
37550
|
+
const absolute = (0, import_node_path3.join)(manifestDir, ...normalized.split("/"));
|
|
37551
|
+
if (absolute !== manifestDir && !absolute.startsWith(manifestDir + import_node_path3.sep)) {
|
|
37552
|
+
throw new Error(`Pack manifest entry escapes the pack root: ${entry}`);
|
|
37553
|
+
}
|
|
37554
|
+
if (await isDirectory(absolute)) {
|
|
37555
|
+
for (const member of await collectBundleMemberFiles(absolute)) {
|
|
37556
|
+
const relativePath = `${normalized}/${member.relativePath}`;
|
|
37557
|
+
members.set(relativePath, { absolutePath: member.absolutePath, relativePath });
|
|
37243
37558
|
}
|
|
37559
|
+
return;
|
|
37244
37560
|
}
|
|
37245
|
-
|
|
37246
|
-
|
|
37247
|
-
|
|
37248
|
-
|
|
37249
|
-
}
|
|
37250
|
-
|
|
37251
|
-
const
|
|
37252
|
-
|
|
37253
|
-
|
|
37254
|
-
|
|
37255
|
-
|
|
37561
|
+
if (await isRegularFileNoSymlink(absolute)) {
|
|
37562
|
+
members.set(normalized, { absolutePath: absolute, relativePath: normalized });
|
|
37563
|
+
return;
|
|
37564
|
+
}
|
|
37565
|
+
throw new Error(`Pack manifest references a missing path: ${entry}`);
|
|
37566
|
+
};
|
|
37567
|
+
for (const skill of manifest.skills) {
|
|
37568
|
+
const skillRel = skill.replace(/\/SKILL\.md$/i, "");
|
|
37569
|
+
const skillDir = (0, import_node_path3.join)(manifestDir, ...skillRel.split("/"));
|
|
37570
|
+
if (!await isFile((0, import_node_path3.join)(skillDir, "SKILL.md"))) {
|
|
37571
|
+
throw new Error(`Pack skill "${skill}" must be a directory containing SKILL.md.`);
|
|
37572
|
+
}
|
|
37573
|
+
await addEntry(skillRel);
|
|
37256
37574
|
}
|
|
37257
|
-
|
|
37575
|
+
for (const include of manifest.include) {
|
|
37576
|
+
await addEntry(include);
|
|
37577
|
+
}
|
|
37578
|
+
return [...members.values()].sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
37258
37579
|
}
|
|
37259
|
-
function
|
|
37260
|
-
return (
|
|
37580
|
+
function escapeRegExp(value) {
|
|
37581
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37261
37582
|
}
|
|
37262
|
-
|
|
37263
|
-
|
|
37264
|
-
|
|
37265
|
-
|
|
37266
|
-
|
|
37267
|
-
|
|
37268
|
-
|
|
37269
|
-
|
|
37270
|
-
state.pendingReindexes = /* @__PURE__ */ new Map();
|
|
37271
|
-
return;
|
|
37272
|
-
}
|
|
37273
|
-
const pending = /* @__PURE__ */ new Map();
|
|
37274
|
-
for (const [team, value] of Object.entries(parsed.teams)) {
|
|
37275
|
-
if (!Array.isArray(value)) {
|
|
37276
|
-
continue;
|
|
37277
|
-
}
|
|
37278
|
-
const changes = [];
|
|
37279
|
-
for (const item of value) {
|
|
37280
|
-
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
37281
|
-
continue;
|
|
37282
|
-
}
|
|
37283
|
-
const entry = item;
|
|
37284
|
-
if (typeof entry.path === "string" && typeof entry.relativePath === "string" && (entry.status === "added" || entry.status === "removed" || entry.status === "modified")) {
|
|
37285
|
-
changes.push({ path: entry.path, relativePath: entry.relativePath, status: entry.status });
|
|
37286
|
-
}
|
|
37583
|
+
function tokenizePackPaths(text, rewriteRoots) {
|
|
37584
|
+
let out = text;
|
|
37585
|
+
for (const root of rewriteRoots) {
|
|
37586
|
+
if (root.length > 0) {
|
|
37587
|
+
out = out.replace(
|
|
37588
|
+
new RegExp(`(?<![A-Za-z0-9/._~-])${escapeRegExp(root)}(?=[/\\\\]|["'\`\\s),>:\\]};=]|$)`, "g"),
|
|
37589
|
+
PACK_ROOT_TOKEN
|
|
37590
|
+
);
|
|
37287
37591
|
}
|
|
37288
|
-
|
|
37289
|
-
|
|
37592
|
+
}
|
|
37593
|
+
return out;
|
|
37594
|
+
}
|
|
37595
|
+
function residualRewriteRoot(content, rewriteRoots) {
|
|
37596
|
+
return rewriteRoots.find((root) => root.length > 0 && content.includes(root));
|
|
37597
|
+
}
|
|
37598
|
+
var PORTABLE_PATH_PREFIXES = [
|
|
37599
|
+
"/usr/",
|
|
37600
|
+
"/bin/",
|
|
37601
|
+
"/sbin/",
|
|
37602
|
+
"/lib/",
|
|
37603
|
+
"/lib64/",
|
|
37604
|
+
"/etc/",
|
|
37605
|
+
"/opt/homebrew/",
|
|
37606
|
+
"/tmp/",
|
|
37607
|
+
"/var/",
|
|
37608
|
+
"/private/var/",
|
|
37609
|
+
"/dev/",
|
|
37610
|
+
"/proc/",
|
|
37611
|
+
"/run/",
|
|
37612
|
+
"/sys/",
|
|
37613
|
+
"/Library/",
|
|
37614
|
+
"/System/",
|
|
37615
|
+
"/Applications/"
|
|
37616
|
+
];
|
|
37617
|
+
function unportableAbsolutePaths(content) {
|
|
37618
|
+
const scan = content.split(`${PACK_ROOT_TOKEN}/`).join("").split(PACK_ROOT_TOKEN).join("");
|
|
37619
|
+
const found = /* @__PURE__ */ new Set();
|
|
37620
|
+
for (const path of scan.match(/(?<![A-Za-z0-9._~$-])\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+/g) ?? []) {
|
|
37621
|
+
if (!PORTABLE_PATH_PREFIXES.some((prefix) => path.startsWith(prefix))) {
|
|
37622
|
+
found.add(path);
|
|
37290
37623
|
}
|
|
37291
37624
|
}
|
|
37292
|
-
|
|
37625
|
+
for (const path of scan.match(/[A-Za-z]:\\[^\s"']+/g) ?? []) {
|
|
37626
|
+
found.add(path);
|
|
37627
|
+
}
|
|
37628
|
+
return [...found].sort((a, b) => compareStrings(a, b));
|
|
37293
37629
|
}
|
|
37294
|
-
async function
|
|
37295
|
-
const
|
|
37296
|
-
|
|
37297
|
-
|
|
37298
|
-
|
|
37630
|
+
async function preparePackMember(config2, team, member, filesTargetDir, rewriteRoots, options) {
|
|
37631
|
+
const buffer = await (0, import_promises4.readFile)(member.absolutePath);
|
|
37632
|
+
const targetPath = (0, import_node_path3.join)(filesTargetDir, ...member.relativePath.split("/"));
|
|
37633
|
+
const targetUri = workfileToVikingUri(config2, team.config, targetPath);
|
|
37634
|
+
if (isProbablyBinary(buffer)) {
|
|
37635
|
+
const credential = detectBinaryCredential(buffer);
|
|
37636
|
+
const localPath = credential === void 0 ? detectBinaryLocalPath(buffer, rewriteRoots) : void 0;
|
|
37637
|
+
let blocker2;
|
|
37638
|
+
if (credential !== void 0) {
|
|
37639
|
+
blocker2 = `possible ${credential} embedded in binary file`;
|
|
37640
|
+
} else if (options.allowBinary !== true) {
|
|
37641
|
+
blocker2 = "binary file (pass --allow-binary to include it unscanned)";
|
|
37642
|
+
} else if (localPath !== void 0) {
|
|
37643
|
+
blocker2 = `possible ${localPath} embedded in binary file (cannot be rewritten)`;
|
|
37644
|
+
}
|
|
37645
|
+
return {
|
|
37646
|
+
binary: true,
|
|
37647
|
+
blocker: blocker2,
|
|
37648
|
+
content: buffer,
|
|
37649
|
+
redactions: [],
|
|
37650
|
+
relativePath: member.relativePath,
|
|
37651
|
+
sha256: sha256(buffer),
|
|
37652
|
+
targetPath,
|
|
37653
|
+
targetUri
|
|
37654
|
+
};
|
|
37299
37655
|
}
|
|
37300
|
-
const
|
|
37301
|
-
|
|
37302
|
-
|
|
37656
|
+
const text = buffer.toString("utf8");
|
|
37657
|
+
if (text.includes(PACK_ROOT_TOKEN)) {
|
|
37658
|
+
return {
|
|
37659
|
+
binary: false,
|
|
37660
|
+
blocker: `contains the reserved ${PACK_ROOT_TOKEN} token`,
|
|
37661
|
+
content: text,
|
|
37662
|
+
redactions: [],
|
|
37663
|
+
relativePath: member.relativePath,
|
|
37664
|
+
sha256: sha256(text),
|
|
37665
|
+
targetPath,
|
|
37666
|
+
targetUri
|
|
37667
|
+
};
|
|
37668
|
+
}
|
|
37669
|
+
const tokenized = tokenizePackPaths(text, rewriteRoots);
|
|
37670
|
+
const isMarkdown = member.relativePath.toLowerCase().endsWith(".md");
|
|
37671
|
+
const scrub = applyScrubber(tokenized, { redact: isMarkdown && options.redact === true });
|
|
37672
|
+
const residual = residualRewriteRoot(scrub.cleaned, rewriteRoots);
|
|
37673
|
+
const blocker = scrub.blocker ?? (residual !== void 0 ? `machine-local path "${residual}" not rewritten` : void 0);
|
|
37674
|
+
return {
|
|
37675
|
+
binary: false,
|
|
37676
|
+
blocker,
|
|
37677
|
+
content: scrub.cleaned,
|
|
37678
|
+
redactions: scrub.redactions,
|
|
37679
|
+
relativePath: member.relativePath,
|
|
37680
|
+
sha256: sha256(scrub.cleaned),
|
|
37681
|
+
targetPath,
|
|
37682
|
+
targetUri
|
|
37303
37683
|
};
|
|
37304
|
-
await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
37305
|
-
const tempPath = `${path}.${process.pid}.tmp`;
|
|
37306
|
-
await (0, import_promises4.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
|
|
37307
|
-
`, { encoding: "utf8", mode: 384 });
|
|
37308
|
-
await (0, import_promises4.rename)(tempPath, path);
|
|
37309
37684
|
}
|
|
37310
|
-
|
|
37311
|
-
const
|
|
37312
|
-
|
|
37313
|
-
|
|
37314
|
-
|
|
37315
|
-
|
|
37316
|
-
|
|
37317
|
-
|
|
37685
|
+
function buildPackIndex(artifact, manifest, skillNames, memberCount) {
|
|
37686
|
+
const lines = [
|
|
37687
|
+
"---",
|
|
37688
|
+
`name: ${artifact.name}`,
|
|
37689
|
+
`agent: ${artifact.agent}`,
|
|
37690
|
+
"kind: pack",
|
|
37691
|
+
`skills: [${skillNames.join(", ")}]`,
|
|
37692
|
+
"---",
|
|
37693
|
+
"",
|
|
37694
|
+
`# ${artifact.name} (skill pack)`,
|
|
37695
|
+
"",
|
|
37696
|
+
manifest.description ?? `A Threadnote skill pack bundling ${skillNames.length} skill(s) and their shared support files (${memberCount} files total).`,
|
|
37697
|
+
"",
|
|
37698
|
+
"## Skills",
|
|
37699
|
+
...skillNames.map((skill) => `- ${skill}`),
|
|
37700
|
+
"",
|
|
37701
|
+
"## Requirements",
|
|
37702
|
+
"Threadnote installs files only. Ensure these exist on the target machine before running:"
|
|
37703
|
+
];
|
|
37704
|
+
if (manifest.deps.runtime.length > 0) {
|
|
37705
|
+
lines.push(`- runtime: ${manifest.deps.runtime.join(", ")}`);
|
|
37318
37706
|
}
|
|
37319
|
-
|
|
37320
|
-
|
|
37321
|
-
const now = Date.now();
|
|
37322
|
-
if (!options.force && state.lastCheckedAt > 0 && now - state.lastCheckedAt < AUTO_SHARE_FETCH_INTERVAL_MS) {
|
|
37323
|
-
return [];
|
|
37324
|
-
}
|
|
37325
|
-
try {
|
|
37326
|
-
const statuses = await fetchShareUpdateStatuses(config2);
|
|
37327
|
-
const nextBehindTeams = new Set(state.behindTeams);
|
|
37328
|
-
for (const status of statuses) {
|
|
37329
|
-
if (status.warning) {
|
|
37330
|
-
continue;
|
|
37331
|
-
}
|
|
37332
|
-
if (status.behind > 0) {
|
|
37333
|
-
nextBehindTeams.add(status.team);
|
|
37334
|
-
} else {
|
|
37335
|
-
nextBehindTeams.delete(status.team);
|
|
37336
|
-
}
|
|
37337
|
-
}
|
|
37338
|
-
state.behindTeams = nextBehindTeams;
|
|
37339
|
-
return statuses.flatMap((status) => status.warning ? [status.warning] : []);
|
|
37340
|
-
} finally {
|
|
37341
|
-
state.lastCheckedAt = Date.now();
|
|
37342
|
-
}
|
|
37343
|
-
}
|
|
37344
|
-
function enqueueShareOperation(state, action) {
|
|
37345
|
-
const previous = state.operationPromise ?? Promise.resolve();
|
|
37346
|
-
const current = previous.catch(() => void 0).then(action);
|
|
37347
|
-
state.operationPromise = current.catch(() => void 0);
|
|
37348
|
-
return current;
|
|
37349
|
-
}
|
|
37350
|
-
async function fetchShareUpdateStatuses(config2) {
|
|
37351
|
-
const teamsFile = await readTeamsFile(config2);
|
|
37352
|
-
const teams = Object.entries(teamsFile.teams);
|
|
37353
|
-
if (teams.length === 0) {
|
|
37354
|
-
return [];
|
|
37355
|
-
}
|
|
37356
|
-
const git = await requiredExecutable("git");
|
|
37357
|
-
const statuses = [];
|
|
37358
|
-
for (const [name, team] of teams) {
|
|
37359
|
-
const fetchResult = await runCommand(git, ["-C", team.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME], {
|
|
37360
|
-
allowFailure: true
|
|
37361
|
-
});
|
|
37362
|
-
if (fetchResult.exitCode !== 0) {
|
|
37363
|
-
statuses.push({
|
|
37364
|
-
behind: 0,
|
|
37365
|
-
team: name,
|
|
37366
|
-
warning: `Auto-sync check for shared team "${name}" failed: ${fetchResult.stderr.trim() || fetchResult.stdout.trim() || "unknown git fetch error"}`
|
|
37367
|
-
});
|
|
37368
|
-
continue;
|
|
37369
|
-
}
|
|
37370
|
-
const behind = await gitOutput(team.worktree, ["rev-list", "--count", "HEAD..@{u}"], false);
|
|
37371
|
-
if (behind === void 0) {
|
|
37372
|
-
statuses.push({
|
|
37373
|
-
behind: 0,
|
|
37374
|
-
team: name,
|
|
37375
|
-
warning: `Auto-sync check for shared team "${name}" failed: could not read upstream behind count.`
|
|
37376
|
-
});
|
|
37377
|
-
continue;
|
|
37378
|
-
}
|
|
37379
|
-
statuses.push({ behind: Number.parseInt(behind, 10) || 0, team: name });
|
|
37380
|
-
}
|
|
37381
|
-
return statuses;
|
|
37382
|
-
}
|
|
37383
|
-
async function runShareSyncQuiet(config2, state, options) {
|
|
37384
|
-
const team = await resolveTeam(config2, options.team);
|
|
37385
|
-
const git = await requiredExecutable("git");
|
|
37386
|
-
const worktree = team.config.worktree;
|
|
37387
|
-
const pendingChanges = state.pendingReindexes.get(team.name);
|
|
37388
|
-
if (pendingChanges && pendingChanges.length > 0) {
|
|
37389
|
-
await applyAndPersistChanges(config2, team.config, state, pendingChanges, { quiet: true });
|
|
37390
|
-
}
|
|
37391
|
-
if (await hasUncommittedChanges(worktree)) {
|
|
37392
|
-
return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
|
|
37707
|
+
if (manifest.deps.cli.length > 0) {
|
|
37708
|
+
lines.push(`- CLI: ${manifest.deps.cli.join(", ")}`);
|
|
37393
37709
|
}
|
|
37394
|
-
|
|
37395
|
-
|
|
37396
|
-
return `Shared team "${team.name}" upstream status is unknown; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to inspect and resolve it.`;
|
|
37397
|
-
}
|
|
37398
|
-
if ((Number.parseInt(ahead, 10) || 0) > 0) {
|
|
37399
|
-
return `Shared team "${team.name}" has local commits ahead of upstream; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or reconcile them.`;
|
|
37400
|
-
}
|
|
37401
|
-
const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
|
|
37402
|
-
const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
|
|
37403
|
-
if (pullResult.exitCode !== 0) {
|
|
37404
|
-
if (await exists((0, import_node_path4.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path4.join)(team.config.gitdir, "rebase-apply"))) {
|
|
37405
|
-
throw new Error(
|
|
37406
|
-
`Automatic share sync hit git conflicts in ${worktree}. Resolve them in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then rerun recall/read.`
|
|
37407
|
-
);
|
|
37408
|
-
}
|
|
37409
|
-
throw new Error(
|
|
37410
|
-
`Automatic share sync failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
|
|
37411
|
-
);
|
|
37710
|
+
if (manifest.deps.os.length > 0) {
|
|
37711
|
+
lines.push(`- OS: ${manifest.deps.os.join(", ")}`);
|
|
37412
37712
|
}
|
|
37413
|
-
|
|
37414
|
-
|
|
37415
|
-
const changes = await listChangedFiles(worktree, beforeRev, afterRev);
|
|
37416
|
-
if (changes.length > 0) {
|
|
37417
|
-
const stillPending = state.pendingReindexes.get(team.name) ?? [];
|
|
37418
|
-
const combined = mergeChanges(stillPending, changes);
|
|
37419
|
-
await applyAndPersistChanges(config2, team.config, state, combined, { quiet: true });
|
|
37420
|
-
}
|
|
37713
|
+
if (manifest.deps.mcp.length > 0) {
|
|
37714
|
+
lines.push(`- MCP (configure separately): ${manifest.deps.mcp.join(", ")}`);
|
|
37421
37715
|
}
|
|
37422
|
-
|
|
37716
|
+
lines.push("", `Install: threadnote share install-artifacts --kind pack --name ${artifact.name} --apply`, "");
|
|
37717
|
+
return lines.join("\n");
|
|
37423
37718
|
}
|
|
37424
|
-
|
|
37425
|
-
const
|
|
37426
|
-
|
|
37427
|
-
|
|
37428
|
-
|
|
37429
|
-
|
|
37430
|
-
|
|
37431
|
-
|
|
37432
|
-
|
|
37433
|
-
messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
|
|
37434
|
-
}
|
|
37435
|
-
if (dryRun) {
|
|
37436
|
-
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "commit", "-m", commitMessage])}`);
|
|
37437
|
-
} else {
|
|
37438
|
-
const commitResult = await runCommand(git, ["-C", worktree, "commit", "-m", commitMessage], { allowFailure: true });
|
|
37439
|
-
if (commitResult.exitCode !== 0) {
|
|
37440
|
-
const detail = commitResult.stdout.trim() || commitResult.stderr.trim();
|
|
37441
|
-
if (/nothing to commit|no changes added/i.test(detail)) {
|
|
37442
|
-
messages.push("git commit: nothing to commit (file already in tree)");
|
|
37443
|
-
} else {
|
|
37444
|
-
throw new Error(`git commit failed: ${detail || "unknown error"}`);
|
|
37445
|
-
}
|
|
37446
|
-
} else {
|
|
37447
|
-
messages.push(`git commit: ${commitResult.stdout.trim().split("\n").slice(0, 2).join(" ")}`);
|
|
37448
|
-
}
|
|
37449
|
-
}
|
|
37450
|
-
if (!push) {
|
|
37451
|
-
messages.push("git push skipped (push=false)");
|
|
37452
|
-
return messages;
|
|
37453
|
-
}
|
|
37454
|
-
const pushResult = await runGitCommand(
|
|
37455
|
-
dryRun,
|
|
37456
|
-
git,
|
|
37457
|
-
["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME],
|
|
37458
|
-
"git push failed"
|
|
37459
|
-
);
|
|
37460
|
-
if (pushResult) {
|
|
37461
|
-
messages.push(`git push: ${pushResult.stdout.trim() || pushResult.stderr.trim() || "ok"}`);
|
|
37462
|
-
}
|
|
37463
|
-
return messages;
|
|
37719
|
+
function buildPackManifestJson(artifact, manifest, prepared) {
|
|
37720
|
+
const data = {
|
|
37721
|
+
artifact,
|
|
37722
|
+
deps: manifest.deps,
|
|
37723
|
+
members: prepared.map((entry) => ({ binary: entry.binary, path: entry.relativePath, sha256: entry.sha256 })).sort((a, b) => compareStrings(a.path, b.path)),
|
|
37724
|
+
version: BUNDLE_MANIFEST_VERSION
|
|
37725
|
+
};
|
|
37726
|
+
return `${JSON.stringify(data, void 0, 2)}
|
|
37727
|
+
`;
|
|
37464
37728
|
}
|
|
37465
|
-
|
|
37466
|
-
|
|
37467
|
-
|
|
37468
|
-
return void 0;
|
|
37469
|
-
}
|
|
37470
|
-
const result = await runCommand(git, args, { allowFailure: true });
|
|
37471
|
-
if (result.exitCode !== 0) {
|
|
37472
|
-
throw new Error(`${failureLabel}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`);
|
|
37473
|
-
}
|
|
37474
|
-
return result;
|
|
37729
|
+
function packSkillName(skillEntry) {
|
|
37730
|
+
const trimmed = skillEntry.replace(/\/SKILL\.md$/i, "");
|
|
37731
|
+
return (0, import_node_path3.basename)(trimmed);
|
|
37475
37732
|
}
|
|
37476
|
-
async function
|
|
37733
|
+
async function shareBundlePack(config2, manifestPath, options) {
|
|
37477
37734
|
const team = await resolveTeam(config2, options.team);
|
|
37478
37735
|
const dryRun = options.dryRun === true;
|
|
37479
37736
|
const preview = options.preview === true;
|
|
37480
|
-
const
|
|
37481
|
-
if (!await isRegularFileNoSymlink(
|
|
37482
|
-
throw new Error(`
|
|
37483
|
-
}
|
|
37484
|
-
const
|
|
37485
|
-
const
|
|
37486
|
-
|
|
37487
|
-
|
|
37488
|
-
|
|
37489
|
-
const
|
|
37490
|
-
const
|
|
37491
|
-
const
|
|
37492
|
-
const
|
|
37737
|
+
const resolvedManifest = expandPath(manifestPath);
|
|
37738
|
+
if (!await isRegularFileNoSymlink(resolvedManifest)) {
|
|
37739
|
+
throw new Error(`Pack manifest is not a regular file: ${resolvedManifest}`);
|
|
37740
|
+
}
|
|
37741
|
+
const manifest = parsePackManifest(await (0, import_promises4.readFile)(resolvedManifest, "utf8"), resolvedManifest);
|
|
37742
|
+
const manifestDir = (0, import_node_path3.dirname)(resolvedManifest);
|
|
37743
|
+
const artifact = { agent: manifest.agent, kind: "pack", name: uriSegment(manifest.name) };
|
|
37744
|
+
const skillNames = manifest.skills.map(packSkillName);
|
|
37745
|
+
const members = await collectPackMembers(manifestDir, manifest);
|
|
37746
|
+
const autoRoots = manifestDir.split("/").filter(Boolean).length >= 2 ? [manifestDir] : [];
|
|
37747
|
+
const rewriteRoots = [.../* @__PURE__ */ new Set([...autoRoots, ...manifest.pathRewrites])].sort((a, b) => b.length - a.length);
|
|
37748
|
+
const packRootRelative = `${SHAREABLE_ARTIFACT_DIR}/packs/${artifact.agent}/${artifact.name}`;
|
|
37749
|
+
const filesRelative = `${packRootRelative}/${PACK_FILES_DIR}`;
|
|
37750
|
+
const indexRelative = `${packRootRelative}/${artifact.name}${PACK_INDEX_SUFFIX}`;
|
|
37751
|
+
const manifestRelative = `${packRootRelative}/${artifact.name}${PACK_MANIFEST_SUFFIX}`;
|
|
37752
|
+
const filesTargetDir = (0, import_node_path3.join)(team.config.worktree, ...filesRelative.split("/"));
|
|
37753
|
+
const packRootTargetDir = (0, import_node_path3.join)(team.config.worktree, ...packRootRelative.split("/"));
|
|
37754
|
+
const indexTargetPath = (0, import_node_path3.join)(team.config.worktree, ...indexRelative.split("/"));
|
|
37755
|
+
const indexTargetUri = workfileToVikingUri(config2, team.config, indexTargetPath);
|
|
37756
|
+
const prepared = await Promise.all(
|
|
37757
|
+
members.map((member) => preparePackMember(config2, team, member, filesTargetDir, rewriteRoots, options))
|
|
37758
|
+
);
|
|
37759
|
+
const indexContent = tokenizePackPaths(buildPackIndex(artifact, manifest, skillNames, prepared.length), rewriteRoots);
|
|
37760
|
+
const indexScrub = applyScrubber(indexContent, { redact: options.redact === true });
|
|
37493
37761
|
const messages = [
|
|
37494
|
-
`${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"}
|
|
37495
|
-
`Source: ${portablePath(
|
|
37496
|
-
`Destination: ${
|
|
37762
|
+
`${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} pack ${artifact.agent}/${artifact.name} (${prepared.length} files, ${skillNames.length} skills)`,
|
|
37763
|
+
`Source: ${portablePath(manifestDir)}`,
|
|
37764
|
+
`Destination: ${indexTargetUri}`
|
|
37497
37765
|
];
|
|
37766
|
+
const blockers = prepared.filter((entry) => entry.blocker !== void 0);
|
|
37498
37767
|
if (preview) {
|
|
37499
|
-
|
|
37500
|
-
|
|
37501
|
-
|
|
37502
|
-
|
|
37503
|
-
|
|
37504
|
-
|
|
37505
|
-
|
|
37506
|
-
targetPath,
|
|
37507
|
-
targetUri
|
|
37508
|
-
};
|
|
37509
|
-
}
|
|
37510
|
-
for (const redaction of scrub.redactions) {
|
|
37511
|
-
messages.push(`PREVIEW redact: ${redaction.count}\xD7 ${redaction.name}`);
|
|
37768
|
+
for (const entry of prepared) {
|
|
37769
|
+
const flags = entry.binary ? ["binary"] : [];
|
|
37770
|
+
for (const redaction of entry.redactions) {
|
|
37771
|
+
flags.push(`redact ${redaction.count}\xD7 ${redaction.name}`);
|
|
37772
|
+
}
|
|
37773
|
+
const note = entry.blocker !== void 0 ? ` BLOCKED: ${entry.blocker}` : "";
|
|
37774
|
+
messages.push(` ${entry.relativePath}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}${note}`);
|
|
37512
37775
|
}
|
|
37513
37776
|
return {
|
|
37514
37777
|
artifact,
|
|
37515
37778
|
gitMessages: [],
|
|
37516
37779
|
messages,
|
|
37517
|
-
previewContent:
|
|
37518
|
-
sourcePath:
|
|
37519
|
-
targetPath,
|
|
37520
|
-
targetUri
|
|
37780
|
+
previewContent: indexScrub.cleaned,
|
|
37781
|
+
sourcePath: resolvedManifest,
|
|
37782
|
+
targetPath: indexTargetPath,
|
|
37783
|
+
targetUri: indexTargetUri
|
|
37521
37784
|
};
|
|
37522
37785
|
}
|
|
37523
|
-
|
|
37786
|
+
const indexResidual = residualRewriteRoot(indexScrub.cleaned, rewriteRoots);
|
|
37787
|
+
if (indexScrub.blocker !== void 0 || indexResidual !== void 0) {
|
|
37524
37788
|
throw new Error(
|
|
37525
|
-
`Refusing to share ${
|
|
37789
|
+
`Refusing to share pack ${artifact.agent}/${artifact.name}: index ${indexScrub.blocker ?? `machine-local path "${indexResidual}" not rewritten`}.`
|
|
37526
37790
|
);
|
|
37527
37791
|
}
|
|
37528
|
-
|
|
37529
|
-
|
|
37792
|
+
if (blockers.length > 0) {
|
|
37793
|
+
throw new Error(
|
|
37794
|
+
`Refusing to share pack ${artifact.agent}/${artifact.name}: ${blockers.map((entry) => `${entry.relativePath} (${entry.blocker})`).join("; ")}. Strip the value, pass --redact for local paths, or --allow-binary for binary files.`
|
|
37795
|
+
);
|
|
37530
37796
|
}
|
|
37531
|
-
const
|
|
37532
|
-
|
|
37533
|
-
|
|
37797
|
+
const packJson = applyScrubber(tokenizePackPaths(buildPackManifestJson(artifact, manifest, prepared), rewriteRoots), {
|
|
37798
|
+
redact: options.redact === true
|
|
37799
|
+
});
|
|
37800
|
+
const packJsonResidual = residualRewriteRoot(packJson.cleaned, rewriteRoots);
|
|
37801
|
+
if (packJson.blocker !== void 0 || packJsonResidual !== void 0) {
|
|
37534
37802
|
throw new Error(
|
|
37535
|
-
`
|
|
37803
|
+
`Refusing to share pack ${artifact.agent}/${artifact.name}: manifest ${packJson.blocker ?? `machine-local path "${packJsonResidual}" not rewritten`}.`
|
|
37804
|
+
);
|
|
37805
|
+
}
|
|
37806
|
+
for (const entry of prepared) {
|
|
37807
|
+
for (const redaction of entry.redactions) {
|
|
37808
|
+
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} in ${entry.relativePath} before sharing.`);
|
|
37809
|
+
}
|
|
37810
|
+
}
|
|
37811
|
+
const unportable = /* @__PURE__ */ new Set();
|
|
37812
|
+
for (const entry of prepared) {
|
|
37813
|
+
if (!entry.binary && typeof entry.content === "string") {
|
|
37814
|
+
for (const path of unportableAbsolutePaths(entry.content)) {
|
|
37815
|
+
unportable.add(`${entry.relativePath}: ${path}`);
|
|
37816
|
+
}
|
|
37817
|
+
}
|
|
37818
|
+
}
|
|
37819
|
+
for (const path of unportableAbsolutePaths(indexScrub.cleaned)) {
|
|
37820
|
+
unportable.add(`${artifact.name}${PACK_INDEX_SUFFIX}: ${path}`);
|
|
37821
|
+
}
|
|
37822
|
+
for (const path of unportableAbsolutePaths(packJson.cleaned)) {
|
|
37823
|
+
unportable.add(`${artifact.name}${PACK_MANIFEST_SUFFIX}: ${path}`);
|
|
37824
|
+
}
|
|
37825
|
+
if (unportable.size > 0) {
|
|
37826
|
+
messages.push(
|
|
37827
|
+
`Warning: possible machine-local absolute path(s) that will not resolve on a teammate's machine (declare in pathRewrites or strip if not portable): ${[...unportable].join("; ")}`
|
|
37536
37828
|
);
|
|
37537
37829
|
}
|
|
37830
|
+
for (const entry of prepared) {
|
|
37831
|
+
const existing = await readFileBytesIfExists(entry.targetPath);
|
|
37832
|
+
if (existing !== void 0 && sha256(existing) !== entry.sha256 && options.force !== true) {
|
|
37833
|
+
throw new Error(
|
|
37834
|
+
`Shared pack file already exists with different content: ${portablePath(entry.targetPath)}. Pass --force to replace it.`
|
|
37835
|
+
);
|
|
37836
|
+
}
|
|
37837
|
+
}
|
|
37538
37838
|
if (dryRun) {
|
|
37539
|
-
messages.push(`Would write
|
|
37839
|
+
messages.push(`Would write ${prepared.length} files under ${portablePath(packRootTargetDir)}`);
|
|
37840
|
+
return {
|
|
37841
|
+
artifact,
|
|
37842
|
+
gitMessages: [],
|
|
37843
|
+
messages,
|
|
37844
|
+
sourcePath: resolvedManifest,
|
|
37845
|
+
targetPath: indexTargetPath,
|
|
37846
|
+
targetUri: indexTargetUri
|
|
37847
|
+
};
|
|
37540
37848
|
}
|
|
37541
37849
|
const ov = await openVikingCliForMode(dryRun);
|
|
37542
|
-
const
|
|
37543
|
-
|
|
37544
|
-
|
|
37545
|
-
|
|
37546
|
-
|
|
37850
|
+
const rollbacks = [];
|
|
37851
|
+
const manifestTargetPath = (0, import_node_path3.join)(team.config.worktree, ...manifestRelative.split("/"));
|
|
37852
|
+
try {
|
|
37853
|
+
const writeMarkdownMember = async (uri, content, worktreePath) => {
|
|
37854
|
+
const priorBytes = await readFileBytesIfExists(worktreePath);
|
|
37855
|
+
const hadResource = await vikingResourceExists(ov, config2, uri);
|
|
37856
|
+
await ensureSharedDirectoryChain(config2, ov, uri, dryRun, { quiet: true });
|
|
37857
|
+
await writeMemoryFile(config2, ov, uri, content, hadResource ? "replace" : "create", dryRun, { quiet: true });
|
|
37858
|
+
rollbacks.push(async () => {
|
|
37859
|
+
if (priorBytes !== void 0) {
|
|
37860
|
+
await writeMemoryFile(config2, ov, uri, priorBytes.toString("utf8"), "replace", false, { quiet: true });
|
|
37861
|
+
} else if (await vikingResourceExists(ov, config2, uri)) {
|
|
37862
|
+
await removeMemoryUri(config2, ov, uri, false, { quiet: true });
|
|
37863
|
+
}
|
|
37864
|
+
});
|
|
37865
|
+
};
|
|
37866
|
+
await writeMarkdownMember(indexTargetUri, indexScrub.cleaned, indexTargetPath);
|
|
37867
|
+
for (const entry of prepared.filter((member) => member.relativePath.endsWith(".md"))) {
|
|
37868
|
+
await writeMarkdownMember(entry.targetUri, entry.content, entry.targetPath);
|
|
37869
|
+
}
|
|
37870
|
+
await ensureDirectory(filesTargetDir, false);
|
|
37871
|
+
for (const entry of prepared.filter((member) => !member.relativePath.endsWith(".md"))) {
|
|
37872
|
+
const priorBytes = await readFileBytesIfExists(entry.targetPath);
|
|
37873
|
+
await ensureDirectory((0, import_node_path3.dirname)(entry.targetPath), false);
|
|
37874
|
+
await (0, import_promises4.writeFile)(entry.targetPath, entry.content, entry.binary ? { mode: 384 } : { encoding: "utf8", mode: 384 });
|
|
37875
|
+
rollbacks.push(async () => {
|
|
37876
|
+
if (priorBytes !== void 0) {
|
|
37877
|
+
await (0, import_promises4.writeFile)(entry.targetPath, priorBytes, { mode: 384 });
|
|
37878
|
+
} else {
|
|
37879
|
+
await (0, import_promises4.rm)(entry.targetPath, { force: true });
|
|
37880
|
+
}
|
|
37881
|
+
});
|
|
37882
|
+
}
|
|
37883
|
+
await ensureDirectory(packRootTargetDir, false);
|
|
37884
|
+
const priorManifest = await readFileBytesIfExists(manifestTargetPath);
|
|
37885
|
+
await (0, import_promises4.writeFile)(manifestTargetPath, packJson.cleaned, { encoding: "utf8", mode: 384 });
|
|
37886
|
+
rollbacks.push(async () => {
|
|
37887
|
+
if (priorManifest !== void 0) {
|
|
37888
|
+
await (0, import_promises4.writeFile)(manifestTargetPath, priorManifest, { mode: 384 });
|
|
37889
|
+
} else {
|
|
37890
|
+
await (0, import_promises4.rm)(manifestTargetPath, { force: true });
|
|
37891
|
+
}
|
|
37892
|
+
});
|
|
37893
|
+
const currentFiles = new Set(prepared.map((entry) => `${filesRelative}/${entry.relativePath}`));
|
|
37894
|
+
const git = await requiredExecutable("git");
|
|
37895
|
+
const tracked = await runCommand(git, ["-C", team.config.worktree, "ls-files", "--", filesRelative], {
|
|
37896
|
+
allowFailure: true
|
|
37897
|
+
});
|
|
37898
|
+
const stalePaths = tracked.exitCode === 0 ? tracked.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !currentFiles.has(line)) : [];
|
|
37899
|
+
for (const stale of stalePaths) {
|
|
37900
|
+
await runCommand(git, ["-C", team.config.worktree, "rm", "-f", "--ignore-unmatch", "--", stale], {
|
|
37901
|
+
allowFailure: true
|
|
37902
|
+
});
|
|
37903
|
+
if (stale.endsWith(".md")) {
|
|
37904
|
+
const staleUri = workfileToVikingUri(config2, team.config, (0, import_node_path3.join)(team.config.worktree, ...stale.split("/")));
|
|
37905
|
+
try {
|
|
37906
|
+
if (await vikingResourceExists(ov, config2, staleUri)) {
|
|
37907
|
+
await removeMemoryUri(config2, ov, staleUri, dryRun, { quiet: true });
|
|
37908
|
+
}
|
|
37909
|
+
} catch (pruneErr) {
|
|
37910
|
+
messages.push(
|
|
37911
|
+
`Warning: could not remove stale OpenViking resource ${staleUri}: ${pruneErr instanceof Error ? pruneErr.message : String(pruneErr)}`
|
|
37912
|
+
);
|
|
37913
|
+
}
|
|
37914
|
+
}
|
|
37915
|
+
}
|
|
37916
|
+
} catch (publishErr) {
|
|
37917
|
+
for (const undo of rollbacks.reverse()) {
|
|
37918
|
+
try {
|
|
37919
|
+
await undo();
|
|
37920
|
+
} catch (_cleanupErr) {
|
|
37921
|
+
}
|
|
37922
|
+
}
|
|
37923
|
+
throw publishErr;
|
|
37924
|
+
}
|
|
37925
|
+
const stagedPaths = [
|
|
37926
|
+
indexRelative,
|
|
37927
|
+
manifestRelative,
|
|
37928
|
+
...prepared.map((entry) => `${filesRelative}/${entry.relativePath}`)
|
|
37929
|
+
];
|
|
37930
|
+
const message = options.message ?? `share: publish pack ${artifact.agent}/${artifact.name} (${prepared.length} files)`;
|
|
37931
|
+
const gitMessages = await publishShareGitChange(team.config.worktree, stagedPaths, message, {
|
|
37547
37932
|
dryRun,
|
|
37548
37933
|
push: options.push
|
|
37549
37934
|
});
|
|
37550
|
-
return {
|
|
37935
|
+
return {
|
|
37936
|
+
artifact,
|
|
37937
|
+
gitMessages,
|
|
37938
|
+
messages,
|
|
37939
|
+
sourcePath: resolvedManifest,
|
|
37940
|
+
targetPath: indexTargetPath,
|
|
37941
|
+
targetUri: indexTargetUri
|
|
37942
|
+
};
|
|
37551
37943
|
}
|
|
37552
37944
|
async function listSharedAgentArtifacts(config2, options = {}) {
|
|
37553
37945
|
const syncResult = await maybeSyncSharedArtifacts(config2, options);
|
|
@@ -37590,6 +37982,10 @@ async function installSharedAgentArtifacts(config2, options) {
|
|
|
37590
37982
|
}
|
|
37591
37983
|
let installedCount = 0;
|
|
37592
37984
|
for (const artifact of artifacts) {
|
|
37985
|
+
if (isBundleArtifact(artifact)) {
|
|
37986
|
+
installedCount += await installBundleArtifact(artifact, options, dryRun, messages);
|
|
37987
|
+
continue;
|
|
37988
|
+
}
|
|
37593
37989
|
const label = sharedArtifactLabel(artifact.artifact);
|
|
37594
37990
|
const state = await sharedArtifactInstallState(artifact);
|
|
37595
37991
|
if (dryRun) {
|
|
@@ -37606,7 +38002,7 @@ async function installSharedAgentArtifacts(config2, options) {
|
|
|
37606
38002
|
messages.push(`Already installed ${label}: ${portablePath(artifact.installPath)}`);
|
|
37607
38003
|
continue;
|
|
37608
38004
|
}
|
|
37609
|
-
await ensureDirectory((0,
|
|
38005
|
+
await ensureDirectory((0, import_node_path3.dirname)(artifact.installPath), false);
|
|
37610
38006
|
await (0, import_promises4.writeFile)(artifact.installPath, state.sourceContent, { encoding: "utf8", mode: 384 });
|
|
37611
38007
|
await writeSharedArtifactMetadata(artifact, state.sourceSha);
|
|
37612
38008
|
installedCount += 1;
|
|
@@ -37635,10 +38031,10 @@ function normalizeTeamName(input) {
|
|
|
37635
38031
|
return candidate;
|
|
37636
38032
|
}
|
|
37637
38033
|
function teamsFilePath(config2) {
|
|
37638
|
-
return (0,
|
|
38034
|
+
return (0, import_node_path3.join)(config2.agentContextHome, "share", "teams.json");
|
|
37639
38035
|
}
|
|
37640
38036
|
function teamWorktreePath(config2, team) {
|
|
37641
|
-
return (0,
|
|
38037
|
+
return (0, import_node_path3.join)(
|
|
37642
38038
|
config2.agentContextHome,
|
|
37643
38039
|
"data",
|
|
37644
38040
|
"viking",
|
|
@@ -37651,7 +38047,7 @@ function teamWorktreePath(config2, team) {
|
|
|
37651
38047
|
);
|
|
37652
38048
|
}
|
|
37653
38049
|
function teamGitdirPath(config2, team) {
|
|
37654
|
-
return (0,
|
|
38050
|
+
return (0, import_node_path3.join)(config2.agentContextHome, "share", "teams", `${team}.gitdir`);
|
|
37655
38051
|
}
|
|
37656
38052
|
async function readTeamsFile(config2) {
|
|
37657
38053
|
const path = teamsFilePath(config2);
|
|
@@ -37707,7 +38103,7 @@ async function resolveTeam(config2, requested) {
|
|
|
37707
38103
|
return { config: found, name: wantName };
|
|
37708
38104
|
}
|
|
37709
38105
|
function workfileToVikingUri(config2, team, filePath) {
|
|
37710
|
-
const rel = (0,
|
|
38106
|
+
const rel = (0, import_node_path3.relative)(team.worktree, filePath).split(import_node_path3.sep).join("/");
|
|
37711
38107
|
return `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
37712
38108
|
}
|
|
37713
38109
|
function isInSharedNamespace(config2, uri) {
|
|
@@ -37765,15 +38161,15 @@ async function isRegularFileNoSymlink(path) {
|
|
|
37765
38161
|
}
|
|
37766
38162
|
}
|
|
37767
38163
|
function inferShareArtifact(path, options) {
|
|
37768
|
-
const normalizedPath = path.split(
|
|
37769
|
-
const fileName = (0,
|
|
38164
|
+
const normalizedPath = path.split(import_node_path3.sep).join("/");
|
|
38165
|
+
const fileName = (0, import_node_path3.basename)(path);
|
|
37770
38166
|
const lowerFileName = fileName.toLowerCase();
|
|
37771
38167
|
const lowerPath = normalizedPath.toLowerCase();
|
|
37772
38168
|
const inferredKind = lowerFileName === "skill.md" ? "skill" : lowerPath.includes("/.claude/commands/") && lowerFileName.endsWith(".md") ? "command" : void 0;
|
|
37773
38169
|
const inferredAgent = lowerPath.includes("/.codex/skills/") ? "codex" : lowerPath.includes("/.claude/skills/") || lowerPath.includes("/.claude/commands/") ? "claude" : void 0;
|
|
37774
38170
|
const extensionIndex = fileName.lastIndexOf(".");
|
|
37775
38171
|
const stem = extensionIndex > 0 ? fileName.slice(0, extensionIndex) : fileName;
|
|
37776
|
-
const inferredName = lowerFileName === "skill.md" ? (0,
|
|
38172
|
+
const inferredName = lowerFileName === "skill.md" ? (0, import_node_path3.basename)((0, import_node_path3.dirname)(path)) : stem;
|
|
37777
38173
|
const kind = options.kind ?? inferredKind;
|
|
37778
38174
|
const agent = options.agent ?? inferredAgent;
|
|
37779
38175
|
const name = options.name ?? inferredName;
|
|
@@ -37814,10 +38210,13 @@ function sharedArtifactFromRelativePath(relativePath) {
|
|
|
37814
38210
|
if (parts.length === 4 && parts[1] === "commands" && parts[2] === "claude" && parts[3].endsWith(".md")) {
|
|
37815
38211
|
return { agent: "claude", kind: "command", name: parts[3].slice(0, -".md".length) };
|
|
37816
38212
|
}
|
|
38213
|
+
if (parts.length === 5 && parts[1] === "packs" && (parts[2] === "codex" || parts[2] === "claude") && parts[4] === `${parts[3]}${PACK_INDEX_SUFFIX}`) {
|
|
38214
|
+
return { agent: parts[2], kind: "pack", name: parts[3] };
|
|
38215
|
+
}
|
|
37817
38216
|
return void 0;
|
|
37818
38217
|
}
|
|
37819
38218
|
async function collectSharedArtifacts(worktree, team) {
|
|
37820
|
-
const root = (0,
|
|
38219
|
+
const root = (0, import_node_path3.join)(worktree, SHAREABLE_ARTIFACT_DIR);
|
|
37821
38220
|
if (!await isDirectory(root)) {
|
|
37822
38221
|
return [];
|
|
37823
38222
|
}
|
|
@@ -37825,7 +38224,7 @@ async function collectSharedArtifacts(worktree, team) {
|
|
|
37825
38224
|
async function visit(path) {
|
|
37826
38225
|
const entries = await (0, import_promises4.readdir)(path, { withFileTypes: true });
|
|
37827
38226
|
for (const entry of entries) {
|
|
37828
|
-
const full = (0,
|
|
38227
|
+
const full = (0, import_node_path3.join)(path, entry.name);
|
|
37829
38228
|
if (entry.isDirectory()) {
|
|
37830
38229
|
await visit(full);
|
|
37831
38230
|
continue;
|
|
@@ -37833,23 +38232,100 @@ async function collectSharedArtifacts(worktree, team) {
|
|
|
37833
38232
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
37834
38233
|
continue;
|
|
37835
38234
|
}
|
|
37836
|
-
const relativePath = (0,
|
|
38235
|
+
const relativePath = (0, import_node_path3.relative)(worktree, full).split(import_node_path3.sep).join("/");
|
|
37837
38236
|
const artifact = sharedArtifactFromRelativePath(relativePath);
|
|
37838
38237
|
if (artifact === void 0) {
|
|
37839
38238
|
continue;
|
|
37840
38239
|
}
|
|
37841
|
-
|
|
37842
|
-
|
|
37843
|
-
|
|
37844
|
-
|
|
37845
|
-
|
|
37846
|
-
|
|
37847
|
-
}
|
|
37848
|
-
|
|
38240
|
+
const artifactDir = (0, import_node_path3.dirname)(full);
|
|
38241
|
+
if (artifact.kind === "pack" && !await isFile((0, import_node_path3.join)(artifactDir, `${artifact.name}${PACK_MANIFEST_SUFFIX}`))) {
|
|
38242
|
+
console.warn(
|
|
38243
|
+
`Skipping incomplete shared pack (missing ${artifact.name}${PACK_MANIFEST_SUFFIX}): ${relativePath}`
|
|
38244
|
+
);
|
|
38245
|
+
continue;
|
|
38246
|
+
}
|
|
38247
|
+
try {
|
|
38248
|
+
out.push({
|
|
38249
|
+
artifact,
|
|
38250
|
+
installPath: sharedArtifactInstallPath(team, artifact),
|
|
38251
|
+
members: await collectArtifactMembers(artifact, artifactDir),
|
|
38252
|
+
sourcePath: full,
|
|
38253
|
+
sourceRelativePath: relativePath,
|
|
38254
|
+
team
|
|
38255
|
+
});
|
|
38256
|
+
} catch (err) {
|
|
38257
|
+
console.warn(`Skipping shared artifact ${relativePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
38258
|
+
}
|
|
38259
|
+
}
|
|
37849
38260
|
}
|
|
37850
38261
|
await visit(root);
|
|
37851
38262
|
return out.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
|
|
37852
38263
|
}
|
|
38264
|
+
async function collectArtifactMembers(artifact, artifactDir) {
|
|
38265
|
+
if (artifact.kind === "skill") {
|
|
38266
|
+
return collectSharedBundleMembers(artifactDir);
|
|
38267
|
+
}
|
|
38268
|
+
if (artifact.kind === "pack") {
|
|
38269
|
+
return collectSharedPackMembers(artifact, artifactDir);
|
|
38270
|
+
}
|
|
38271
|
+
return void 0;
|
|
38272
|
+
}
|
|
38273
|
+
function isContainedMemberPath(baseDir, relativePath) {
|
|
38274
|
+
if ((0, import_node_path3.isAbsolute)(relativePath) || relativePath.split("/").includes("..")) {
|
|
38275
|
+
return false;
|
|
38276
|
+
}
|
|
38277
|
+
const resolved = (0, import_node_path3.join)(baseDir, ...relativePath.split("/"));
|
|
38278
|
+
return resolved === baseDir || resolved.startsWith(baseDir + import_node_path3.sep);
|
|
38279
|
+
}
|
|
38280
|
+
async function collectSharedPackMembers(artifact, packDir) {
|
|
38281
|
+
const filesDir = (0, import_node_path3.join)(packDir, PACK_FILES_DIR);
|
|
38282
|
+
if (!await isDirectory(filesDir)) {
|
|
38283
|
+
return [];
|
|
38284
|
+
}
|
|
38285
|
+
const manifestRaw = await readFileIfExists((0, import_node_path3.join)(packDir, `${artifact.name}${PACK_MANIFEST_SUFFIX}`));
|
|
38286
|
+
if (manifestRaw !== void 0) {
|
|
38287
|
+
const rawMembers = parseJsonConfigObject(manifestRaw)?.members;
|
|
38288
|
+
if (Array.isArray(rawMembers)) {
|
|
38289
|
+
const fromManifest = [];
|
|
38290
|
+
for (const entry of rawMembers) {
|
|
38291
|
+
const path = entry?.path;
|
|
38292
|
+
if (typeof path === "string" && path.length > 0) {
|
|
38293
|
+
if (!isContainedMemberPath(filesDir, path)) {
|
|
38294
|
+
throw new Error(`Refusing pack member with an unsafe path that escapes the pack root: ${path}`);
|
|
38295
|
+
}
|
|
38296
|
+
fromManifest.push({ absolutePath: (0, import_node_path3.join)(filesDir, ...path.split("/")), relativePath: path });
|
|
38297
|
+
}
|
|
38298
|
+
}
|
|
38299
|
+
if (fromManifest.length > 0) {
|
|
38300
|
+
return fromManifest.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
38301
|
+
}
|
|
38302
|
+
}
|
|
38303
|
+
}
|
|
38304
|
+
return collectBundleMemberFiles(filesDir);
|
|
38305
|
+
}
|
|
38306
|
+
async function collectSharedBundleMembers(skillDir) {
|
|
38307
|
+
const manifestRaw = await readFileIfExists((0, import_node_path3.join)(skillDir, BUNDLE_MANIFEST_FILE));
|
|
38308
|
+
if (manifestRaw !== void 0) {
|
|
38309
|
+
const parsed = parseJsonConfigObject(manifestRaw);
|
|
38310
|
+
const rawMembers = parsed?.members;
|
|
38311
|
+
if (Array.isArray(rawMembers)) {
|
|
38312
|
+
const fromManifest = [];
|
|
38313
|
+
for (const entry of rawMembers) {
|
|
38314
|
+
const path = entry?.path;
|
|
38315
|
+
if (typeof path === "string" && path.length > 0) {
|
|
38316
|
+
if (!isContainedMemberPath(skillDir, path)) {
|
|
38317
|
+
throw new Error(`Refusing skill member with an unsafe path that escapes the skill root: ${path}`);
|
|
38318
|
+
}
|
|
38319
|
+
fromManifest.push({ absolutePath: (0, import_node_path3.join)(skillDir, ...path.split("/")), relativePath: path });
|
|
38320
|
+
}
|
|
38321
|
+
}
|
|
38322
|
+
if (fromManifest.length > 0) {
|
|
38323
|
+
return fromManifest.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
38324
|
+
}
|
|
38325
|
+
}
|
|
38326
|
+
}
|
|
38327
|
+
return collectBundleMemberFiles(skillDir);
|
|
38328
|
+
}
|
|
37853
38329
|
function filterSharedArtifacts(artifacts, options) {
|
|
37854
38330
|
const name = options.name === void 0 ? void 0 : uriSegment(options.name);
|
|
37855
38331
|
return artifacts.filter((artifact) => {
|
|
@@ -37872,8 +38348,223 @@ async function maybeSyncSharedArtifacts(config2, options) {
|
|
|
37872
38348
|
return syncSharedReposBeforeAgentRead(config2);
|
|
37873
38349
|
}
|
|
37874
38350
|
async function sharedArtifactInstallStatus(artifact) {
|
|
38351
|
+
if (isBundleArtifact(artifact)) {
|
|
38352
|
+
return sharedBundleInstallStatus(artifact);
|
|
38353
|
+
}
|
|
37875
38354
|
return (await sharedArtifactInstallState(artifact)).status;
|
|
37876
38355
|
}
|
|
38356
|
+
function bundleInstallRoot(artifact) {
|
|
38357
|
+
return artifact.artifact.kind === "pack" ? artifact.installPath : (0, import_node_path3.dirname)(artifact.installPath);
|
|
38358
|
+
}
|
|
38359
|
+
function bundleInstallMetadataPath(artifact) {
|
|
38360
|
+
return (0, import_node_path3.join)(bundleInstallRoot(artifact), BUNDLE_INSTALL_METADATA_FILE);
|
|
38361
|
+
}
|
|
38362
|
+
async function readBundleInstallMetadata(artifact) {
|
|
38363
|
+
const raw = await readFileIfExists(bundleInstallMetadataPath(artifact));
|
|
38364
|
+
if (raw === void 0) {
|
|
38365
|
+
return void 0;
|
|
38366
|
+
}
|
|
38367
|
+
const parsed = parseJsonConfigObject(raw);
|
|
38368
|
+
if (parsed === void 0 || parsed.version !== ARTIFACT_INSTALL_METADATA_VERSION || !Array.isArray(parsed.members)) {
|
|
38369
|
+
return void 0;
|
|
38370
|
+
}
|
|
38371
|
+
const recordedArtifact = parsed.artifact;
|
|
38372
|
+
if (recordedArtifact?.agent !== artifact.artifact.agent || recordedArtifact?.kind !== artifact.artifact.kind || recordedArtifact?.name !== artifact.artifact.name || parsed.team !== artifact.team) {
|
|
38373
|
+
return void 0;
|
|
38374
|
+
}
|
|
38375
|
+
const map3 = /* @__PURE__ */ new Map();
|
|
38376
|
+
for (const entry of parsed.members) {
|
|
38377
|
+
const path = entry?.path;
|
|
38378
|
+
const sourceSha256 = entry?.sourceSha256;
|
|
38379
|
+
const installedSha256 = entry?.installedSha256;
|
|
38380
|
+
if (typeof path === "string" && typeof sourceSha256 === "string" && typeof installedSha256 === "string") {
|
|
38381
|
+
map3.set(path, { installedSha256, sourceSha256 });
|
|
38382
|
+
}
|
|
38383
|
+
}
|
|
38384
|
+
return map3;
|
|
38385
|
+
}
|
|
38386
|
+
async function sharedBundleInstallStatus(artifact) {
|
|
38387
|
+
const members = artifact.members ?? [];
|
|
38388
|
+
const installRoot = bundleInstallRoot(artifact);
|
|
38389
|
+
const metadata = await readBundleInstallMetadata(artifact);
|
|
38390
|
+
const expanded = await prepareInstallMembers(members, installRoot, artifact.artifact.kind === "pack");
|
|
38391
|
+
const expectedByPath = new Map(expanded.map((entry) => [entry.relativePath, entry]));
|
|
38392
|
+
let installedCount = 0;
|
|
38393
|
+
let localChanged = false;
|
|
38394
|
+
let remoteChanged = false;
|
|
38395
|
+
const memberPaths = /* @__PURE__ */ new Set();
|
|
38396
|
+
for (const member of members) {
|
|
38397
|
+
memberPaths.add(member.relativePath);
|
|
38398
|
+
const expected = expectedByPath.get(member.relativePath);
|
|
38399
|
+
if (expected === void 0) {
|
|
38400
|
+
remoteChanged = true;
|
|
38401
|
+
continue;
|
|
38402
|
+
}
|
|
38403
|
+
const installedBytes = await readFileBytesIfExists((0, import_node_path3.join)(installRoot, ...member.relativePath.split("/")));
|
|
38404
|
+
if (installedBytes === void 0) {
|
|
38405
|
+
remoteChanged = true;
|
|
38406
|
+
continue;
|
|
38407
|
+
}
|
|
38408
|
+
installedCount += 1;
|
|
38409
|
+
const installedSha = sha256(installedBytes);
|
|
38410
|
+
const recorded = metadata?.get(member.relativePath);
|
|
38411
|
+
if (recorded === void 0) {
|
|
38412
|
+
if (installedSha !== expected?.installedSha256) {
|
|
38413
|
+
localChanged = true;
|
|
38414
|
+
}
|
|
38415
|
+
continue;
|
|
38416
|
+
}
|
|
38417
|
+
if (installedSha !== recorded.installedSha256) {
|
|
38418
|
+
localChanged = true;
|
|
38419
|
+
}
|
|
38420
|
+
if (expected?.sourceSha256 !== recorded.sourceSha256) {
|
|
38421
|
+
remoteChanged = true;
|
|
38422
|
+
}
|
|
38423
|
+
}
|
|
38424
|
+
if (metadata !== void 0) {
|
|
38425
|
+
for (const [recordedPath, recorded] of metadata) {
|
|
38426
|
+
if (memberPaths.has(recordedPath)) {
|
|
38427
|
+
continue;
|
|
38428
|
+
}
|
|
38429
|
+
remoteChanged = true;
|
|
38430
|
+
const installedBytes = await readFileBytesIfExists((0, import_node_path3.join)(installRoot, ...recordedPath.split("/")));
|
|
38431
|
+
if (installedBytes !== void 0 && sha256(installedBytes) !== recorded.installedSha256) {
|
|
38432
|
+
localChanged = true;
|
|
38433
|
+
}
|
|
38434
|
+
}
|
|
38435
|
+
}
|
|
38436
|
+
if (installedCount === 0 && metadata === void 0) {
|
|
38437
|
+
return "not_installed";
|
|
38438
|
+
}
|
|
38439
|
+
if (localChanged && remoteChanged) {
|
|
38440
|
+
return "remote_changed_and_local_modified";
|
|
38441
|
+
}
|
|
38442
|
+
if (remoteChanged) {
|
|
38443
|
+
return "update_available";
|
|
38444
|
+
}
|
|
38445
|
+
if (localChanged) {
|
|
38446
|
+
return "local_modified";
|
|
38447
|
+
}
|
|
38448
|
+
return "current";
|
|
38449
|
+
}
|
|
38450
|
+
function expandPackRoot(text, installRoot) {
|
|
38451
|
+
return text.split(PACK_ROOT_TOKEN).join(installRoot);
|
|
38452
|
+
}
|
|
38453
|
+
async function prepareInstallMembers(members, installRoot, expandTokens) {
|
|
38454
|
+
const prepared = await Promise.all(
|
|
38455
|
+
members.map(async (member) => {
|
|
38456
|
+
const sourceBytes = await readFileBytesIfExists(member.absolutePath);
|
|
38457
|
+
if (sourceBytes === void 0) {
|
|
38458
|
+
return void 0;
|
|
38459
|
+
}
|
|
38460
|
+
const installedBytes = expandTokens && !isProbablyBinary(sourceBytes) ? Buffer.from(expandPackRoot(sourceBytes.toString("utf8"), installRoot), "utf8") : sourceBytes;
|
|
38461
|
+
return {
|
|
38462
|
+
installedBytes,
|
|
38463
|
+
installedSha256: sha256(installedBytes),
|
|
38464
|
+
relativePath: member.relativePath,
|
|
38465
|
+
sourceSha256: sha256(sourceBytes)
|
|
38466
|
+
};
|
|
38467
|
+
})
|
|
38468
|
+
);
|
|
38469
|
+
return prepared.filter((member) => member !== void 0);
|
|
38470
|
+
}
|
|
38471
|
+
function serializeInstallMetadata(artifact, prepared) {
|
|
38472
|
+
const metadata = {
|
|
38473
|
+
artifact: artifact.artifact,
|
|
38474
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
38475
|
+
members: prepared.map((entry) => ({
|
|
38476
|
+
installedSha256: entry.installedSha256,
|
|
38477
|
+
path: entry.relativePath,
|
|
38478
|
+
sourceSha256: entry.sourceSha256
|
|
38479
|
+
})).sort((a, b) => compareStrings(a.path, b.path)),
|
|
38480
|
+
team: artifact.team,
|
|
38481
|
+
version: ARTIFACT_INSTALL_METADATA_VERSION
|
|
38482
|
+
};
|
|
38483
|
+
return `${JSON.stringify(metadata, void 0, 2)}
|
|
38484
|
+
`;
|
|
38485
|
+
}
|
|
38486
|
+
async function installBundleArtifact(artifact, options, dryRun, messages) {
|
|
38487
|
+
const members = artifact.members ?? [];
|
|
38488
|
+
const installRoot = bundleInstallRoot(artifact);
|
|
38489
|
+
const kindLabel = artifact.artifact.kind === "pack" ? "pack" : "bundle";
|
|
38490
|
+
const label = `${sharedArtifactLabel(artifact.artifact)} ${kindLabel} (${members.length} files)`;
|
|
38491
|
+
const status = await sharedBundleInstallStatus(artifact);
|
|
38492
|
+
if (dryRun) {
|
|
38493
|
+
const verb = sharedArtifactDryRunVerb(status, options.force === true);
|
|
38494
|
+
const suffix = sharedArtifactDryRunSuffix(status, options.force === true);
|
|
38495
|
+
messages.push(`${verb} ${label}: ${portablePath(installRoot)}${suffix}`);
|
|
38496
|
+
return 0;
|
|
38497
|
+
}
|
|
38498
|
+
if ((status === "local_modified" || status === "remote_changed_and_local_modified") && options.force !== true) {
|
|
38499
|
+
throw new Error(`Refusing to overwrite ${portablePath(installRoot)}. Pass force=true or --force.`);
|
|
38500
|
+
}
|
|
38501
|
+
const prepared = await prepareInstallMembers(members, installRoot, artifact.artifact.kind === "pack");
|
|
38502
|
+
if (prepared.length < members.length && options.force !== true) {
|
|
38503
|
+
throw new Error(
|
|
38504
|
+
`Refusing to install ${portablePath(installRoot)}: ${members.length - prepared.length} declared member(s) are unreadable in the shared pack (the shared worktree may be mid-sync). Retry after sync, or pass force=true / --force.`
|
|
38505
|
+
);
|
|
38506
|
+
}
|
|
38507
|
+
if (status === "current") {
|
|
38508
|
+
await (0, import_promises4.writeFile)(bundleInstallMetadataPath(artifact), serializeInstallMetadata(artifact, prepared), { mode: 384 });
|
|
38509
|
+
messages.push(`Already installed ${label}: ${portablePath(installRoot)}`);
|
|
38510
|
+
await surfacePackRequirements(artifact, messages);
|
|
38511
|
+
return 0;
|
|
38512
|
+
}
|
|
38513
|
+
const stagingRoot = `${installRoot}.threadnote-staging`;
|
|
38514
|
+
await (0, import_promises4.rm)(stagingRoot, { force: true, recursive: true });
|
|
38515
|
+
for (const entry of prepared) {
|
|
38516
|
+
const dest = (0, import_node_path3.join)(stagingRoot, ...entry.relativePath.split("/"));
|
|
38517
|
+
await ensureDirectory((0, import_node_path3.dirname)(dest), false);
|
|
38518
|
+
await (0, import_promises4.writeFile)(dest, entry.installedBytes, { mode: 384 });
|
|
38519
|
+
}
|
|
38520
|
+
await (0, import_promises4.writeFile)((0, import_node_path3.join)(stagingRoot, BUNDLE_INSTALL_METADATA_FILE), serializeInstallMetadata(artifact, prepared), {
|
|
38521
|
+
mode: 384
|
|
38522
|
+
});
|
|
38523
|
+
await ensureDirectory((0, import_node_path3.dirname)(installRoot), false);
|
|
38524
|
+
const backupRoot = `${installRoot}.threadnote-old`;
|
|
38525
|
+
await (0, import_promises4.rm)(backupRoot, { force: true, recursive: true });
|
|
38526
|
+
const hadPriorInstall = await exists(installRoot);
|
|
38527
|
+
if (hadPriorInstall) {
|
|
38528
|
+
await (0, import_promises4.rename)(installRoot, backupRoot);
|
|
38529
|
+
}
|
|
38530
|
+
try {
|
|
38531
|
+
await (0, import_promises4.rename)(stagingRoot, installRoot);
|
|
38532
|
+
} catch (swapErr) {
|
|
38533
|
+
if (hadPriorInstall) {
|
|
38534
|
+
await (0, import_promises4.rename)(backupRoot, installRoot);
|
|
38535
|
+
}
|
|
38536
|
+
throw swapErr;
|
|
38537
|
+
}
|
|
38538
|
+
await (0, import_promises4.rm)(backupRoot, { force: true, recursive: true });
|
|
38539
|
+
messages.push(`${sharedArtifactInstallVerb(status, options.force === true)} ${label}: ${portablePath(installRoot)}`);
|
|
38540
|
+
await surfacePackRequirements(artifact, messages);
|
|
38541
|
+
return 1;
|
|
38542
|
+
}
|
|
38543
|
+
async function surfacePackRequirements(artifact, messages) {
|
|
38544
|
+
if (artifact.artifact.kind !== "pack") {
|
|
38545
|
+
return;
|
|
38546
|
+
}
|
|
38547
|
+
const raw = await readFileIfExists(
|
|
38548
|
+
(0, import_node_path3.join)((0, import_node_path3.dirname)(artifact.sourcePath), `${artifact.artifact.name}${PACK_MANIFEST_SUFFIX}`)
|
|
38549
|
+
);
|
|
38550
|
+
if (raw === void 0) {
|
|
38551
|
+
return;
|
|
38552
|
+
}
|
|
38553
|
+
const deps = parseJsonConfigObject(raw)?.deps;
|
|
38554
|
+
if (deps === void 0 || typeof deps !== "object") {
|
|
38555
|
+
return;
|
|
38556
|
+
}
|
|
38557
|
+
const stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
38558
|
+
const depsRecord = deps;
|
|
38559
|
+
const tooling = [...stringList(depsRecord.runtime), ...stringList(depsRecord.cli), ...stringList(depsRecord.os)];
|
|
38560
|
+
const mcp = stringList(depsRecord.mcp);
|
|
38561
|
+
if (tooling.length > 0) {
|
|
38562
|
+
messages.push(`This pack will NOT run until these exist (Threadnote installs files only): ${tooling.join(", ")}.`);
|
|
38563
|
+
}
|
|
38564
|
+
if (mcp.length > 0) {
|
|
38565
|
+
messages.push(`Configure these MCP server(s) separately: ${mcp.join(", ")}.`);
|
|
38566
|
+
}
|
|
38567
|
+
}
|
|
37877
38568
|
async function sharedArtifactInstallState(artifact) {
|
|
37878
38569
|
const sourceContent = await (0, import_promises4.readFile)(artifact.sourcePath, "utf8");
|
|
37879
38570
|
const sourceSha = sha256(sourceContent);
|
|
@@ -37944,7 +38635,7 @@ async function writeSharedArtifactMetadata(artifact, sourceSha) {
|
|
|
37944
38635
|
version: ARTIFACT_INSTALL_METADATA_VERSION
|
|
37945
38636
|
};
|
|
37946
38637
|
const metadataPath = sharedArtifactMetadataPath(artifact);
|
|
37947
|
-
await ensureDirectory((0,
|
|
38638
|
+
await ensureDirectory((0, import_node_path3.dirname)(metadataPath), false);
|
|
37948
38639
|
await (0, import_promises4.writeFile)(metadataPath, `${JSON.stringify(metadata, void 0, 2)}
|
|
37949
38640
|
`, { encoding: "utf8", mode: 384 });
|
|
37950
38641
|
}
|
|
@@ -37996,336 +38687,834 @@ function sharedArtifactLabel(artifact) {
|
|
|
37996
38687
|
return `${artifact.kind} ${artifact.agent}/${artifact.name}`;
|
|
37997
38688
|
}
|
|
37998
38689
|
function sharedArtifactInstallPath(team, artifact) {
|
|
37999
|
-
|
|
38000
|
-
|
|
38690
|
+
const agentDir = artifact.agent === "codex" ? ".codex" : ".claude";
|
|
38691
|
+
if (artifact.kind === "pack") {
|
|
38692
|
+
return (0, import_node_path3.join)((0, import_node_os2.homedir)(), agentDir, "skills", "threadnote-packs", team, artifact.name);
|
|
38693
|
+
}
|
|
38694
|
+
if (artifact.kind === "skill") {
|
|
38695
|
+
return (0, import_node_path3.join)((0, import_node_os2.homedir)(), agentDir, "skills", "threadnote", team, artifact.name, "SKILL.md");
|
|
38696
|
+
}
|
|
38697
|
+
return (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".claude", "commands", "threadnote", team, `${artifact.name}.md`);
|
|
38698
|
+
}
|
|
38699
|
+
function stripPersonalProvenance(content) {
|
|
38700
|
+
const lines = content.split("\n");
|
|
38701
|
+
let headerEnd = lines.length;
|
|
38702
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
38703
|
+
if (lines[index].trim() === "") {
|
|
38704
|
+
headerEnd = index;
|
|
38705
|
+
break;
|
|
38706
|
+
}
|
|
38707
|
+
}
|
|
38708
|
+
const cleaned = [];
|
|
38709
|
+
for (let index = 0; index < headerEnd; index += 1) {
|
|
38710
|
+
if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
|
|
38711
|
+
continue;
|
|
38712
|
+
}
|
|
38713
|
+
cleaned.push(lines[index]);
|
|
38001
38714
|
}
|
|
38002
|
-
|
|
38003
|
-
|
|
38715
|
+
for (let index = headerEnd; index < lines.length; index += 1) {
|
|
38716
|
+
cleaned.push(lines[index]);
|
|
38717
|
+
}
|
|
38718
|
+
return cleaned.join("\n");
|
|
38719
|
+
}
|
|
38720
|
+
async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun, options = {}) {
|
|
38721
|
+
const directoryUri = parentUri(memoryUri);
|
|
38722
|
+
for (const uri of sharedDirectoryChain(config2, directoryUri)) {
|
|
38723
|
+
const args = withIdentity(config2, ["stat", uri]);
|
|
38724
|
+
if (dryRun) {
|
|
38725
|
+
if (options.quiet !== true) {
|
|
38726
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
38727
|
+
}
|
|
38728
|
+
continue;
|
|
38729
|
+
}
|
|
38730
|
+
const statResult = await runCommand(ov, args, { allowFailure: true });
|
|
38731
|
+
if (statResult.exitCode === 0) {
|
|
38732
|
+
continue;
|
|
38733
|
+
}
|
|
38734
|
+
if (options.quiet === true) {
|
|
38735
|
+
await runCommand(ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared context."]));
|
|
38736
|
+
} else {
|
|
38737
|
+
await maybeRun(false, ov, withIdentity(config2, ["mkdir", uri, "--description", "Threadnote shared context."]));
|
|
38738
|
+
}
|
|
38739
|
+
}
|
|
38740
|
+
}
|
|
38741
|
+
function parentUri(uri) {
|
|
38742
|
+
const lastSlash = uri.lastIndexOf("/");
|
|
38743
|
+
return lastSlash === -1 ? uri : uri.slice(0, lastSlash);
|
|
38744
|
+
}
|
|
38745
|
+
function sharedDirectoryChain(config2, directoryUri) {
|
|
38746
|
+
const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
|
|
38747
|
+
if (!directoryUri.startsWith(prefix)) {
|
|
38748
|
+
return [directoryUri];
|
|
38749
|
+
}
|
|
38750
|
+
const parts = directoryUri.slice(prefix.length).split("/").filter(Boolean);
|
|
38751
|
+
const chain = [];
|
|
38752
|
+
for (let index = 1; index <= parts.length; index += 1) {
|
|
38753
|
+
chain.push(`${prefix}${parts.slice(0, index).join("/")}`);
|
|
38754
|
+
}
|
|
38755
|
+
return chain;
|
|
38756
|
+
}
|
|
38757
|
+
async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun, options = {}) {
|
|
38758
|
+
if (dryRun) {
|
|
38759
|
+
const args = withIdentity(config2, [
|
|
38760
|
+
"write",
|
|
38761
|
+
uri,
|
|
38762
|
+
"--from-file",
|
|
38763
|
+
"<staged temp file>",
|
|
38764
|
+
"--mode",
|
|
38765
|
+
initialMode,
|
|
38766
|
+
"--wait",
|
|
38767
|
+
"--timeout",
|
|
38768
|
+
"120"
|
|
38769
|
+
]);
|
|
38770
|
+
if (options.quiet !== true) {
|
|
38771
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
38772
|
+
}
|
|
38773
|
+
return;
|
|
38774
|
+
}
|
|
38775
|
+
const stagingDir = await (0, import_promises4.mkdtemp)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "threadnote-share-"));
|
|
38776
|
+
const tempPath = (0, import_node_path3.join)(stagingDir, "body.txt");
|
|
38777
|
+
try {
|
|
38778
|
+
await (0, import_promises4.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
|
|
38779
|
+
await writeOvFileWithRetry(config2, ov, uri, tempPath, initialMode, options);
|
|
38780
|
+
await refreshMemoryIndex(config2, ov, uri, options);
|
|
38781
|
+
} finally {
|
|
38782
|
+
await (0, import_promises4.rm)(stagingDir, { force: true, recursive: true });
|
|
38783
|
+
}
|
|
38784
|
+
}
|
|
38785
|
+
var BUSY_RETRY_BACKOFF_MS = [2e3, 5e3, 1e4, 2e4, 3e4];
|
|
38786
|
+
async function writeOvFileWithRetry(config2, ov, uri, fromFile, initialMode, options = {}) {
|
|
38787
|
+
const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
|
|
38788
|
+
const existedBeforeWrite = await vikingResourceExists(ov, config2, uri);
|
|
38789
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
38790
|
+
const existsNow = attempt === 0 ? existedBeforeWrite : await vikingResourceExists(ov, config2, uri);
|
|
38791
|
+
const ourWriteLanded = existsNow && !existedBeforeWrite;
|
|
38792
|
+
const mode = attempt === 0 ? initialMode : ourWriteLanded ? "replace" : initialMode;
|
|
38793
|
+
const args = withIdentity(config2, [
|
|
38794
|
+
"write",
|
|
38795
|
+
uri,
|
|
38796
|
+
"--from-file",
|
|
38797
|
+
fromFile,
|
|
38798
|
+
"--mode",
|
|
38799
|
+
mode,
|
|
38800
|
+
"--wait",
|
|
38801
|
+
"--timeout",
|
|
38802
|
+
"120"
|
|
38803
|
+
]);
|
|
38804
|
+
if (options.quiet !== true) {
|
|
38805
|
+
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
38806
|
+
}
|
|
38807
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
38808
|
+
if (result.exitCode === 0) {
|
|
38809
|
+
if (options.quiet !== true && result.stdout.trim()) {
|
|
38810
|
+
console.log(result.stdout.trim());
|
|
38811
|
+
}
|
|
38812
|
+
if (options.quiet !== true && result.stderr.trim()) {
|
|
38813
|
+
console.error(result.stderr.trim());
|
|
38814
|
+
}
|
|
38815
|
+
return;
|
|
38816
|
+
}
|
|
38817
|
+
if (isTransientOvFailure(result.stderr, result.stdout) && await vikingResourceExists(ov, config2, uri) && !existedBeforeWrite) {
|
|
38818
|
+
if (options.quiet !== true) {
|
|
38819
|
+
console.log(
|
|
38820
|
+
"OpenViking accepted the write but returned an error before the wait completed; draining the queue."
|
|
38821
|
+
);
|
|
38822
|
+
}
|
|
38823
|
+
await waitForOvQueue(ov, config2, options);
|
|
38824
|
+
return;
|
|
38825
|
+
}
|
|
38826
|
+
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
38827
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
38828
|
+
}
|
|
38829
|
+
if (isResourceBusyFailure(result.stderr, result.stdout)) {
|
|
38830
|
+
await waitForOvQueue(ov, config2, options);
|
|
38831
|
+
await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
|
|
38832
|
+
} else {
|
|
38833
|
+
await sleep(1e3 * (attempt + 1));
|
|
38834
|
+
}
|
|
38835
|
+
}
|
|
38836
|
+
}
|
|
38837
|
+
async function refreshMemoryIndex(config2, ov, uri, options = {}) {
|
|
38838
|
+
const result = await runCommand(
|
|
38839
|
+
ov,
|
|
38840
|
+
withIdentity(config2, ["reindex", uri, "--mode", "semantic_and_vectors", "--wait", "true"]),
|
|
38841
|
+
{ allowFailure: true }
|
|
38842
|
+
);
|
|
38843
|
+
if (result.exitCode === 0) {
|
|
38844
|
+
if (options.quiet !== true && result.stdout.trim()) {
|
|
38845
|
+
console.log(result.stdout.trim());
|
|
38846
|
+
}
|
|
38847
|
+
if (options.quiet !== true && result.stderr.trim()) {
|
|
38848
|
+
console.error(result.stderr.trim());
|
|
38849
|
+
}
|
|
38850
|
+
return;
|
|
38851
|
+
}
|
|
38852
|
+
if (options.quiet !== true) {
|
|
38853
|
+
console.error(
|
|
38854
|
+
`Memory stored, but index refresh failed for ${uri}: ${result.stderr.trim() || result.stdout.trim()}`
|
|
38855
|
+
);
|
|
38856
|
+
}
|
|
38857
|
+
}
|
|
38858
|
+
async function waitForOvQueue(ov, config2, options = {}) {
|
|
38859
|
+
const result = await runCommand(ov, withIdentity(config2, ["wait", "--timeout", "120"]), { allowFailure: true });
|
|
38860
|
+
if (options.quiet !== true && result.stdout.trim()) {
|
|
38861
|
+
console.log(result.stdout.trim());
|
|
38862
|
+
}
|
|
38863
|
+
if (options.quiet !== true && result.stderr.trim()) {
|
|
38864
|
+
console.error(result.stderr.trim());
|
|
38865
|
+
}
|
|
38866
|
+
}
|
|
38867
|
+
function isTransientOvFailure(stderr, stdout2) {
|
|
38868
|
+
const output = `${stderr}
|
|
38869
|
+
${stdout2}`.toLowerCase();
|
|
38870
|
+
return output.includes("resource is busy") || output.includes("resource is being processed") || output.includes("network error") || output.includes("error sending request") || output.includes("http request failed") || output.includes("connection refused") || output.includes("connection reset") || output.includes("timed out");
|
|
38871
|
+
}
|
|
38872
|
+
function isResourceBusyFailure(stderr, stdout2) {
|
|
38873
|
+
const output = `${stderr}
|
|
38874
|
+
${stdout2}`.toLowerCase();
|
|
38875
|
+
return output.includes("resource is busy") || output.includes("resource is being processed");
|
|
38876
|
+
}
|
|
38877
|
+
async function ingestSingleFile(ov, config2, uri, filePath, initialMode, options = {}) {
|
|
38878
|
+
const content = await (0, import_promises4.readFile)(filePath, "utf8");
|
|
38879
|
+
await writeMemoryFile(config2, ov, uri, content, initialMode, false, options);
|
|
38880
|
+
}
|
|
38881
|
+
async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
|
|
38882
|
+
const args = withIdentity(config2, ["rm", uri]);
|
|
38883
|
+
if (dryRun) {
|
|
38884
|
+
if (options.quiet !== true) {
|
|
38885
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
38886
|
+
}
|
|
38887
|
+
return;
|
|
38888
|
+
}
|
|
38889
|
+
const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
|
|
38890
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
38891
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
38892
|
+
if (result.exitCode === 0) {
|
|
38893
|
+
if (options.quiet !== true && result.stdout.trim()) {
|
|
38894
|
+
console.log(result.stdout.trim());
|
|
38895
|
+
}
|
|
38896
|
+
return;
|
|
38897
|
+
}
|
|
38898
|
+
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
38899
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
38900
|
+
}
|
|
38901
|
+
if (isResourceBusyFailure(result.stderr, result.stdout)) {
|
|
38902
|
+
await waitForOvQueue(ov, config2, options);
|
|
38903
|
+
await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
|
|
38904
|
+
} else {
|
|
38905
|
+
await sleep(1e3 * (attempt + 1));
|
|
38906
|
+
}
|
|
38907
|
+
}
|
|
38908
|
+
}
|
|
38909
|
+
async function vikingResourceExists(ov, config2, uri) {
|
|
38910
|
+
const result = await runCommand(ov, withIdentity(config2, ["stat", uri]), { allowFailure: true });
|
|
38911
|
+
return result.exitCode === 0;
|
|
38912
|
+
}
|
|
38913
|
+
async function hasUncommittedChanges(worktree) {
|
|
38914
|
+
const result = await runCommand("git", ["-C", worktree, "status", "--porcelain"], { allowFailure: true });
|
|
38915
|
+
return result.stdout.trim().length > 0;
|
|
38916
|
+
}
|
|
38917
|
+
async function gitOutput(worktree, args, dryRun) {
|
|
38918
|
+
if (dryRun) {
|
|
38919
|
+
return void 0;
|
|
38920
|
+
}
|
|
38921
|
+
const result = await runCommand("git", ["-C", worktree, ...args], { allowFailure: true });
|
|
38922
|
+
if (result.exitCode !== 0) {
|
|
38923
|
+
return void 0;
|
|
38924
|
+
}
|
|
38925
|
+
return result.stdout.trim();
|
|
38926
|
+
}
|
|
38927
|
+
async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
38928
|
+
const result = await runCommand("git", ["-C", worktree, "diff", "--name-status", "-z", `${beforeRev}..${afterRev}`], {
|
|
38929
|
+
allowFailure: true
|
|
38930
|
+
});
|
|
38931
|
+
if (result.exitCode !== 0) {
|
|
38932
|
+
return [];
|
|
38933
|
+
}
|
|
38934
|
+
const entries = result.stdout.split("\0").filter((part) => part.length > 0);
|
|
38935
|
+
const changes = [];
|
|
38936
|
+
for (let index = 0; index < entries.length; ) {
|
|
38937
|
+
const raw = entries[index];
|
|
38938
|
+
const head = raw.slice(0, 1);
|
|
38939
|
+
if (head === "R" || head === "C") {
|
|
38940
|
+
const oldRel = entries[index + 1];
|
|
38941
|
+
const newRel = entries[index + 2];
|
|
38942
|
+
if (oldRel && newRel) {
|
|
38943
|
+
changes.push({ path: (0, import_node_path3.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
38944
|
+
changes.push({ path: (0, import_node_path3.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
38945
|
+
}
|
|
38946
|
+
index += 3;
|
|
38947
|
+
continue;
|
|
38948
|
+
}
|
|
38949
|
+
const rel = entries[index + 1];
|
|
38950
|
+
if (rel) {
|
|
38951
|
+
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
38952
|
+
changes.push({ path: (0, import_node_path3.join)(worktree, rel), relativePath: rel, status });
|
|
38953
|
+
}
|
|
38954
|
+
index += 2;
|
|
38955
|
+
}
|
|
38956
|
+
return changes;
|
|
38957
|
+
}
|
|
38958
|
+
async function applyChangesToOpenViking(config2, team, changes, options = {}) {
|
|
38959
|
+
const ov = await openVikingCliForMode(false);
|
|
38960
|
+
const failed = [];
|
|
38961
|
+
for (const change of changes) {
|
|
38962
|
+
if (!change.relativePath.endsWith(".md")) {
|
|
38963
|
+
continue;
|
|
38964
|
+
}
|
|
38965
|
+
const firstSegment = change.relativePath.split("/")[0];
|
|
38966
|
+
if (!SHAREABLE_TOP_LEVEL_DIRS.includes(firstSegment)) {
|
|
38967
|
+
continue;
|
|
38968
|
+
}
|
|
38969
|
+
const uri = workfileToVikingUri(config2, team, change.path);
|
|
38970
|
+
try {
|
|
38971
|
+
if (change.status === "removed") {
|
|
38972
|
+
await removeMemoryUri(config2, ov, uri, false, options);
|
|
38973
|
+
continue;
|
|
38974
|
+
}
|
|
38975
|
+
if (!await isFile(change.path)) {
|
|
38976
|
+
continue;
|
|
38977
|
+
}
|
|
38978
|
+
const ovHasResource = await vikingResourceExists(ov, config2, uri);
|
|
38979
|
+
if (ovHasResource) {
|
|
38980
|
+
const reason = change.status === "modified" ? "overwriting local with upstream (local edits to the shared subtree are not preserved across sync)" : "aligning OV to upstream (resource pre-existed in OV, likely from an earlier local publish or sync)";
|
|
38981
|
+
if (options.quiet !== true) {
|
|
38982
|
+
console.warn(`share sync: ${uri}: ${reason}.`);
|
|
38983
|
+
}
|
|
38984
|
+
}
|
|
38985
|
+
await ensureSharedDirectoryChain(config2, ov, uri, false, options);
|
|
38986
|
+
const writeMode = ovHasResource ? "replace" : "create";
|
|
38987
|
+
await ingestSingleFile(ov, config2, uri, change.path, writeMode, options);
|
|
38988
|
+
} catch (err) {
|
|
38989
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38990
|
+
if (options.quiet !== true) {
|
|
38991
|
+
console.warn(`share sync: ${uri}: ingest failed \u2014 will retry on the next sync. ${message}`);
|
|
38992
|
+
}
|
|
38993
|
+
failed.push(change);
|
|
38994
|
+
}
|
|
38995
|
+
}
|
|
38996
|
+
return { failed };
|
|
38997
|
+
}
|
|
38998
|
+
function mergeChanges(...lists) {
|
|
38999
|
+
const map3 = /* @__PURE__ */ new Map();
|
|
39000
|
+
for (const list of lists) {
|
|
39001
|
+
for (const change of list) {
|
|
39002
|
+
map3.set(change.relativePath, change);
|
|
39003
|
+
}
|
|
39004
|
+
}
|
|
39005
|
+
return [...map3.values()];
|
|
39006
|
+
}
|
|
39007
|
+
async function applyAndPersistChanges(config2, team, state, changes, options = {}) {
|
|
39008
|
+
if (changes.length === 0) {
|
|
39009
|
+
return { failed: [] };
|
|
39010
|
+
}
|
|
39011
|
+
state.pendingReindexes.set(team.name, changes);
|
|
39012
|
+
await writePendingReindexes(config2, state);
|
|
39013
|
+
const result = await applyChangesToOpenViking(config2, team, changes, options);
|
|
39014
|
+
if (result.failed.length > 0) {
|
|
39015
|
+
state.pendingReindexes.set(team.name, result.failed);
|
|
39016
|
+
} else {
|
|
39017
|
+
state.pendingReindexes.delete(team.name);
|
|
39018
|
+
}
|
|
39019
|
+
await writePendingReindexes(config2, state);
|
|
39020
|
+
return result;
|
|
39021
|
+
}
|
|
39022
|
+
|
|
39023
|
+
// src/onboarding.ts
|
|
39024
|
+
async function gatherOnboardingContext(config2) {
|
|
39025
|
+
return { seededProjects: await safeSeededProjects(config2), teams: await safeTeams(config2) };
|
|
39026
|
+
}
|
|
39027
|
+
async function safeTeams(config2) {
|
|
39028
|
+
try {
|
|
39029
|
+
return Object.keys((await readTeamsFile(config2)).teams ?? {}).sort();
|
|
39030
|
+
} catch (_err) {
|
|
39031
|
+
return [];
|
|
39032
|
+
}
|
|
39033
|
+
}
|
|
39034
|
+
async function safeSeededProjects(config2) {
|
|
39035
|
+
try {
|
|
39036
|
+
return (await readSeedManifest(config2.manifestPath)).projects.map((project) => project.name);
|
|
39037
|
+
} catch (_err) {
|
|
39038
|
+
return [];
|
|
39039
|
+
}
|
|
39040
|
+
}
|
|
39041
|
+
function buildOnboardingGuide(state) {
|
|
39042
|
+
const serverLine = state.serverUp === false ? "OpenViking is NOT responding \u2014 recall/remember will fail until the user runs `threadnote start`. Offer to walk them through that first." : state.serverUp === true ? "OpenViking is running." : "OpenViking status unknown \u2014 if recall/remember return connection errors, the fix is `threadnote start`.";
|
|
39043
|
+
const teamLine = state.teams.length > 0 ? `Team sharing is configured: ${state.teams.join(", ")}. They can publish memories and skills to teammates.` : "No share team configured yet \u2014 team sharing is available but needs a one-time `threadnote share init <git-remote>`.";
|
|
39044
|
+
const seedLine = state.seededProjects.length > 0 ? `Seeded project guidance is available for: ${state.seededProjects.join(", ")} (recall surfaces it when the query names the project).` : "No seeded project guidance yet \u2014 recall draws on personal memories and shared team memories.";
|
|
39045
|
+
const hasTeam = state.teams.length > 0;
|
|
39046
|
+
return [
|
|
39047
|
+
"# Threadnote \u2014 what you can do here",
|
|
39048
|
+
"",
|
|
39049
|
+
"You are onboarding the user to Threadnote. Do NOT paste this list verbatim. Read it,",
|
|
39050
|
+
"then guide the user conversationally: lead with what is most useful given the state",
|
|
39051
|
+
"below, explain one capability in a sentence, and OFFER to run it for them (with their",
|
|
39052
|
+
"go-ahead) instead of only describing it. Start small.",
|
|
39053
|
+
"",
|
|
39054
|
+
"## Current setup",
|
|
39055
|
+
`- ${serverLine}`,
|
|
39056
|
+
`- ${teamLine}`,
|
|
39057
|
+
`- ${seedLine}`,
|
|
39058
|
+
"",
|
|
39059
|
+
"## Capabilities to offer",
|
|
39060
|
+
"",
|
|
39061
|
+
"Recall context \u2014 pull back the last handoff and durable knowledge for the current",
|
|
39062
|
+
"repo+branch before starting work, so the session continues where the last one left off.",
|
|
39063
|
+
' Run: recall_context({"query":"<repo> latest handoff","callerCwd":"<abs cwd>"}), then',
|
|
39064
|
+
" read_context the most relevant viking:// URI it returns.",
|
|
39065
|
+
"",
|
|
39066
|
+
"Capture work \u2014 save a durable fact (a decision, interface, gotcha) or a handoff (current",
|
|
39067
|
+
"status + next step) so the next session or a teammate inherits it.",
|
|
39068
|
+
' Run: remember_context({"text":"...","kind":"durable","project":"<repo>","topic":"<feature>"})',
|
|
39069
|
+
' or a handoff with kind:"handoff". Reuse the same project/topic to keep one memory current.',
|
|
39070
|
+
"",
|
|
39071
|
+
"Tidy memory \u2014 when recall surfaces overlapping notes for one topic, preview a scoped merge.",
|
|
39072
|
+
' Run: compact_context({"project":"<repo>","topic":"<topic>","dryRun":true}) and review before applying.',
|
|
39073
|
+
"",
|
|
39074
|
+
"Share with your team \u2014 publish a durable memory teammates\u2019 agents can recall. Secrets are",
|
|
39075
|
+
"scrubbed/blocked; handoffs/preferences/local-path notes are never shared.",
|
|
39076
|
+
hasTeam ? ' Run: share_publish({"uri":"viking://user/<you>/memories/durable/projects/<p>/<m>.md"}).' : ' First (one-time): `threadnote share init git@github.com:org/team-memories.git`, then share_publish({"uri":"..."}).',
|
|
39077
|
+
"",
|
|
39078
|
+
"Share skills & packs \u2014 publish a Codex/Claude skill, or a multi-skill pack (skills + shared",
|
|
39079
|
+
"scripts), into the team catalog; teammates install them on demand.",
|
|
39080
|
+
' Run: share_skill({"path":"~/.claude/skills/<name>/SKILL.md"}) or',
|
|
39081
|
+
' share_bundle({"path":"<repo>/threadnote-bundle.json"}). Teammates: list_shared_skills({})',
|
|
39082
|
+
' then install_shared_skill({"name":"<name>"}).',
|
|
39083
|
+
"",
|
|
39084
|
+
"Setup & health \u2014 verify the local install and server.",
|
|
39085
|
+
state.serverUp === false ? " Run: `threadnote start` to bring OpenViking up." : " Run: `threadnote doctor` to check prerequisites.",
|
|
39086
|
+
"",
|
|
39087
|
+
"## How to proceed",
|
|
39088
|
+
"Pick the single most useful step for right now given the setup above (if the server is",
|
|
39089
|
+
"down, that first; otherwise a recall for the current repo is the usual starting point),",
|
|
39090
|
+
"describe it in one sentence, and ask whether to run it. Then chain into the others as the",
|
|
39091
|
+
"user shows interest. Keep it interactive \u2014 one offer at a time, not a wall of options."
|
|
39092
|
+
].join("\n");
|
|
39093
|
+
}
|
|
39094
|
+
|
|
39095
|
+
// src/memory_hygiene.ts
|
|
39096
|
+
var import_node_path4 = require("node:path");
|
|
39097
|
+
var HYGIENE_SOURCES_HEADING = "## Threadnote Hygiene Sources";
|
|
39098
|
+
var STALE_HANDOFF_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
39099
|
+
function parseMemoryDocument(uri, content) {
|
|
39100
|
+
const trimmed = content.trim();
|
|
39101
|
+
if (!trimmed) {
|
|
39102
|
+
return void 0;
|
|
39103
|
+
}
|
|
39104
|
+
const separatorIndex = trimmed.indexOf("\n\n");
|
|
39105
|
+
const header = separatorIndex === -1 ? trimmed : trimmed.slice(0, separatorIndex);
|
|
39106
|
+
const body = separatorIndex === -1 ? "" : trimmed.slice(separatorIndex + 2).trim();
|
|
39107
|
+
const firstLine = header.split("\n")[0]?.trim();
|
|
39108
|
+
if (firstLine !== "MEMORY" && firstLine !== "HANDOFF") {
|
|
39109
|
+
return void 0;
|
|
39110
|
+
}
|
|
39111
|
+
const kind = parseOptionalMemoryKind(headerValue(header, "kind")) ?? (firstLine === "HANDOFF" ? "handoff" : void 0);
|
|
39112
|
+
const status = parseOptionalMemoryStatus(headerValue(header, "status")) ?? "active";
|
|
39113
|
+
if (!kind) {
|
|
39114
|
+
return void 0;
|
|
39115
|
+
}
|
|
39116
|
+
return {
|
|
39117
|
+
body,
|
|
39118
|
+
content: trimmed,
|
|
39119
|
+
headerTitle: firstLine,
|
|
39120
|
+
metadata: {
|
|
39121
|
+
archivedFrom: headerValue(header, "archived_from"),
|
|
39122
|
+
kind,
|
|
39123
|
+
project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
|
|
39124
|
+
sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
|
|
39125
|
+
status,
|
|
39126
|
+
supersedes: headerValue(header, "supersedes"),
|
|
39127
|
+
timestamp: headerValue(header, "timestamp") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
39128
|
+
topic: normalizeOptionalMetadata(headerValue(header, "topic"))
|
|
39129
|
+
},
|
|
39130
|
+
uri
|
|
39131
|
+
};
|
|
39132
|
+
}
|
|
39133
|
+
function buildCompactPlan(records, options) {
|
|
39134
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
39135
|
+
const groupedRecords = records.map((record2) => groupableRecord(record2)).filter((item) => item !== void 0).filter((item) => item.project === options.project).filter((item) => options.topic === void 0 || item.topic === options.topic).filter((item) => options.kind === void 0 || item.record.metadata.kind === options.kind);
|
|
39136
|
+
const groups = /* @__PURE__ */ new Map();
|
|
39137
|
+
for (const item of groupedRecords) {
|
|
39138
|
+
groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
|
|
39139
|
+
}
|
|
39140
|
+
const keepUpdates = [];
|
|
39141
|
+
const archives = [];
|
|
39142
|
+
const forgets = [];
|
|
39143
|
+
const manualReview = [];
|
|
39144
|
+
for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
|
|
39145
|
+
const recordsInGroup = group.map((item) => item.record);
|
|
39146
|
+
const kind = recordsInGroup[0]?.metadata.kind;
|
|
39147
|
+
const topic = group[0]?.topic;
|
|
39148
|
+
const project = group[0]?.project;
|
|
39149
|
+
if (!kind || !project || !isCompactableKind(kind)) {
|
|
39150
|
+
continue;
|
|
39151
|
+
}
|
|
39152
|
+
const duplicateGroups = groupBy(recordsInGroup, (record2) => comparableMemoryBody(record2.body));
|
|
39153
|
+
const duplicateForgetUris = /* @__PURE__ */ new Set();
|
|
39154
|
+
const distinctBodyCount = duplicateGroups.size;
|
|
39155
|
+
for (const duplicateGroup of duplicateGroups.values()) {
|
|
39156
|
+
if (duplicateGroup.length < 2) {
|
|
39157
|
+
continue;
|
|
39158
|
+
}
|
|
39159
|
+
const duplicateKeep = preferredKeepRecord(duplicateGroup, topic);
|
|
39160
|
+
for (const duplicate of sortedNewestFirst(duplicateGroup).filter((record2) => record2.uri !== duplicateKeep.uri)) {
|
|
39161
|
+
duplicateForgetUris.add(duplicate.uri);
|
|
39162
|
+
forgets.push({ reason: `exact duplicate of ${duplicateKeep.uri}`, uri: duplicate.uri });
|
|
39163
|
+
}
|
|
39164
|
+
if (distinctBodyCount === 1 || kind !== "handoff") {
|
|
39165
|
+
keepUpdates.push({
|
|
39166
|
+
content: memoryContentWithHygieneSources(
|
|
39167
|
+
duplicateKeep,
|
|
39168
|
+
duplicateGroup.map((record2) => record2.uri)
|
|
39169
|
+
),
|
|
39170
|
+
reason: "keep exact duplicate group with source URIs",
|
|
39171
|
+
sourceUris: duplicateGroup.map((record2) => record2.uri),
|
|
39172
|
+
uri: duplicateKeep.uri
|
|
39173
|
+
});
|
|
39174
|
+
}
|
|
39175
|
+
}
|
|
39176
|
+
const remainingRecords = recordsInGroup.filter((record2) => !duplicateForgetUris.has(record2.uri));
|
|
39177
|
+
if (recordsInGroup.length > 1 && distinctBodyCount === 1) {
|
|
39178
|
+
continue;
|
|
39179
|
+
}
|
|
39180
|
+
if (remainingRecords.length === 0) {
|
|
39181
|
+
continue;
|
|
39182
|
+
}
|
|
39183
|
+
if (remainingRecords.length === 1) {
|
|
39184
|
+
const [record2] = remainingRecords;
|
|
39185
|
+
if (!record2) {
|
|
39186
|
+
continue;
|
|
39187
|
+
}
|
|
39188
|
+
if (record2.metadata.supersedes === record2.uri) {
|
|
39189
|
+
keepUpdates.push({
|
|
39190
|
+
content: memoryContentWithHygieneSources(record2, [record2.uri]),
|
|
39191
|
+
reason: "strip self-supersedes header",
|
|
39192
|
+
sourceUris: [record2.uri],
|
|
39193
|
+
uri: record2.uri
|
|
39194
|
+
});
|
|
39195
|
+
}
|
|
39196
|
+
if (isStaleLookingHandoff(record2, now)) {
|
|
39197
|
+
manualReview.push({ reason: "stale-looking active handoff", uri: record2.uri });
|
|
39198
|
+
}
|
|
39199
|
+
continue;
|
|
39200
|
+
}
|
|
39201
|
+
if (kind === "handoff") {
|
|
39202
|
+
const keep = preferredKeepRecord(remainingRecords, topic);
|
|
39203
|
+
const sourceUris = recordsInGroup.map((record2) => record2.uri);
|
|
39204
|
+
keepUpdates.push({
|
|
39205
|
+
content: memoryContentWithHygieneSources(keep, sourceUris),
|
|
39206
|
+
reason: "keep latest handoff and preserve source URIs",
|
|
39207
|
+
sourceUris,
|
|
39208
|
+
uri: keep.uri
|
|
39209
|
+
});
|
|
39210
|
+
for (const record2 of sortedNewestFirst(remainingRecords).filter((item) => item.uri !== keep.uri)) {
|
|
39211
|
+
archives.push({
|
|
39212
|
+
kind,
|
|
39213
|
+
project,
|
|
39214
|
+
reason: `older handoff for ${project}/${topic ?? "unknown"}`,
|
|
39215
|
+
topic,
|
|
39216
|
+
uri: record2.uri
|
|
39217
|
+
});
|
|
39218
|
+
}
|
|
39219
|
+
continue;
|
|
39220
|
+
}
|
|
39221
|
+
for (const record2 of sortedNewestFirst(remainingRecords)) {
|
|
39222
|
+
manualReview.push({ reason: `non-exact ${kind} memory in overlapping group`, uri: record2.uri });
|
|
39223
|
+
}
|
|
39224
|
+
}
|
|
39225
|
+
return {
|
|
39226
|
+
archives: dedupeByUri(archives),
|
|
39227
|
+
forgets: dedupeByUri(forgets),
|
|
39228
|
+
keepUpdates: dedupeByUri(keepUpdates),
|
|
39229
|
+
manualReview: dedupeByUri(manualReview),
|
|
39230
|
+
options,
|
|
39231
|
+
recordsScanned: groupedRecords.length
|
|
39232
|
+
};
|
|
39233
|
+
}
|
|
39234
|
+
function memoryContentWithHygieneSources(record2, sourceUris) {
|
|
39235
|
+
const body = stripHygieneSources(record2.body);
|
|
39236
|
+
const uniqueSourceUris = [...new Set(sourceUris)].sort();
|
|
39237
|
+
const metadata = {
|
|
39238
|
+
...record2.metadata,
|
|
39239
|
+
supersedes: record2.metadata.supersedes === record2.uri ? void 0 : record2.metadata.supersedes
|
|
39240
|
+
};
|
|
39241
|
+
return formatMemoryDocument(
|
|
39242
|
+
record2.headerTitle,
|
|
39243
|
+
metadata,
|
|
39244
|
+
[body, "", HYGIENE_SOURCES_HEADING, "", ...uniqueSourceUris.map((uri) => `- ${uri}`)].join("\n")
|
|
39245
|
+
);
|
|
39246
|
+
}
|
|
39247
|
+
function formatCompactPlan(plan, options) {
|
|
39248
|
+
const scope = [
|
|
39249
|
+
`project ${plan.options.project}`,
|
|
39250
|
+
plan.options.topic ? `topic ${plan.options.topic}` : void 0,
|
|
39251
|
+
plan.options.kind ? `kind ${plan.options.kind}` : void 0
|
|
39252
|
+
].filter((item) => item !== void 0).join(", ");
|
|
39253
|
+
const lines = [
|
|
39254
|
+
`${options.apply ? "Applying" : "Dry-run"} memory hygiene plan for ${scope}`,
|
|
39255
|
+
`Records scanned: ${plan.recordsScanned}`,
|
|
39256
|
+
"",
|
|
39257
|
+
formatPlanSection(
|
|
39258
|
+
"Keep/update",
|
|
39259
|
+
plan.keepUpdates.map((action) => `${action.uri} (${action.reason}; sources: ${action.sourceUris.length})`)
|
|
39260
|
+
),
|
|
39261
|
+
formatPlanSection(
|
|
39262
|
+
"Archive old handoffs",
|
|
39263
|
+
plan.archives.map((action) => `${action.uri} (${action.reason})`)
|
|
39264
|
+
),
|
|
39265
|
+
formatPlanSection(
|
|
39266
|
+
"Forget exact duplicates",
|
|
39267
|
+
plan.forgets.map((action) => `${action.uri} (${action.reason})`)
|
|
39268
|
+
),
|
|
39269
|
+
formatPlanSection(
|
|
39270
|
+
"Manual review",
|
|
39271
|
+
plan.manualReview.map((item) => `${item.uri} (${item.reason})`)
|
|
39272
|
+
)
|
|
39273
|
+
];
|
|
39274
|
+
if (!options.apply) {
|
|
39275
|
+
lines.push("", "No changes made. Re-run with --apply to execute this plan.");
|
|
38004
39276
|
}
|
|
38005
|
-
return
|
|
39277
|
+
return lines.join("\n");
|
|
38006
39278
|
}
|
|
38007
|
-
function
|
|
38008
|
-
const
|
|
38009
|
-
|
|
38010
|
-
|
|
38011
|
-
|
|
38012
|
-
|
|
38013
|
-
|
|
39279
|
+
function recallHygieneNudges(text, options) {
|
|
39280
|
+
const activeUris = activePersonalMemoryUrisFromText(text, options.user);
|
|
39281
|
+
const nudges = [];
|
|
39282
|
+
const returnedUriSet = new Set(activeUris);
|
|
39283
|
+
const returnedRecords = options.records?.filter((record2) => returnedUriSet.has(record2.uri)).map((record2) => groupableRecord(record2)) ?? [];
|
|
39284
|
+
const groups = /* @__PURE__ */ new Map();
|
|
39285
|
+
for (const item of returnedRecords) {
|
|
39286
|
+
if (!item) {
|
|
39287
|
+
continue;
|
|
38014
39288
|
}
|
|
39289
|
+
groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
|
|
38015
39290
|
}
|
|
38016
|
-
const
|
|
38017
|
-
|
|
38018
|
-
if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
|
|
39291
|
+
for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
|
|
39292
|
+
if (group.length < 3) {
|
|
38019
39293
|
continue;
|
|
38020
39294
|
}
|
|
38021
|
-
|
|
38022
|
-
|
|
38023
|
-
for (let index = headerEnd; index < lines.length; index += 1) {
|
|
38024
|
-
cleaned.push(lines[index]);
|
|
38025
|
-
}
|
|
38026
|
-
return cleaned.join("\n");
|
|
38027
|
-
}
|
|
38028
|
-
async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun, options = {}) {
|
|
38029
|
-
const directoryUri = parentUri(memoryUri);
|
|
38030
|
-
for (const uri of sharedDirectoryChain(config2, directoryUri)) {
|
|
38031
|
-
const args = withIdentity(config2, ["stat", uri]);
|
|
38032
|
-
if (dryRun) {
|
|
38033
|
-
if (options.quiet !== true) {
|
|
38034
|
-
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
38035
|
-
}
|
|
39295
|
+
const first = group[0];
|
|
39296
|
+
if (!first || !isCompactableKind(first.record.metadata.kind)) {
|
|
38036
39297
|
continue;
|
|
38037
39298
|
}
|
|
38038
|
-
const
|
|
38039
|
-
if (
|
|
39299
|
+
const topic = first.topic;
|
|
39300
|
+
if (!topic) {
|
|
38040
39301
|
continue;
|
|
38041
39302
|
}
|
|
38042
|
-
|
|
38043
|
-
|
|
38044
|
-
|
|
38045
|
-
|
|
39303
|
+
nudges.push(
|
|
39304
|
+
`${group.length} active ${memoryKindPlural(first.record.metadata.kind)} look overlapping for ${first.project}/${topic}; run compact_context({"project":"${first.project}","topic":"${topic}","dryRun":true}).`
|
|
39305
|
+
);
|
|
39306
|
+
}
|
|
39307
|
+
const projectCounts = /* @__PURE__ */ new Map();
|
|
39308
|
+
for (const uri of activeUris) {
|
|
39309
|
+
const parsed = parsePersonalMemoryUri(uri, options.user);
|
|
39310
|
+
if (!parsed || parsed.kind !== "handoff" || parsed.status !== "active") {
|
|
39311
|
+
continue;
|
|
39312
|
+
}
|
|
39313
|
+
projectCounts.set(parsed.project, (projectCounts.get(parsed.project) ?? 0) + 1);
|
|
39314
|
+
}
|
|
39315
|
+
for (const [project, count] of [...projectCounts.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
39316
|
+
if (count < 10) {
|
|
39317
|
+
continue;
|
|
38046
39318
|
}
|
|
39319
|
+
nudges.push(
|
|
39320
|
+
`Many active handoffs surfaced for ${project}; run compact_context({"project":"${project}","dryRun":true}).`
|
|
39321
|
+
);
|
|
38047
39322
|
}
|
|
39323
|
+
return [...new Set(nudges)];
|
|
38048
39324
|
}
|
|
38049
|
-
function
|
|
38050
|
-
const
|
|
38051
|
-
|
|
39325
|
+
function activePersonalMemoryUrisFromText(text, user) {
|
|
39326
|
+
const userSegment = uriSegment(user);
|
|
39327
|
+
const matches = text.matchAll(/viking:\/\/[^\s)]+/g);
|
|
39328
|
+
const uris = [];
|
|
39329
|
+
for (const match of matches) {
|
|
39330
|
+
const uri = match[0]?.replace(/[.,;:]+$/g, "");
|
|
39331
|
+
if (!uri || !parsePersonalMemoryUri(uri, userSegment)) {
|
|
39332
|
+
continue;
|
|
39333
|
+
}
|
|
39334
|
+
uris.push(uri);
|
|
39335
|
+
}
|
|
39336
|
+
return [...new Set(uris)];
|
|
38052
39337
|
}
|
|
38053
|
-
function
|
|
38054
|
-
const prefix = `viking://user/${uriSegment(
|
|
38055
|
-
if (!
|
|
38056
|
-
return
|
|
39338
|
+
function parsePersonalMemoryUri(uri, user) {
|
|
39339
|
+
const prefix = `viking://user/${uriSegment(user)}/memories/`;
|
|
39340
|
+
if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
|
|
39341
|
+
return void 0;
|
|
38057
39342
|
}
|
|
38058
|
-
const
|
|
38059
|
-
const
|
|
38060
|
-
|
|
38061
|
-
|
|
39343
|
+
const rest = uri.slice(prefix.length);
|
|
39344
|
+
const parts = rest.split("/").filter(Boolean);
|
|
39345
|
+
if (parts.length < 4) {
|
|
39346
|
+
return void 0;
|
|
38062
39347
|
}
|
|
38063
|
-
|
|
38064
|
-
}
|
|
38065
|
-
async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun, options = {}) {
|
|
38066
|
-
if (dryRun) {
|
|
38067
|
-
const args = withIdentity(config2, [
|
|
38068
|
-
"write",
|
|
38069
|
-
uri,
|
|
38070
|
-
"--from-file",
|
|
38071
|
-
"<staged temp file>",
|
|
38072
|
-
"--mode",
|
|
38073
|
-
initialMode,
|
|
38074
|
-
"--wait",
|
|
38075
|
-
"--timeout",
|
|
38076
|
-
"120"
|
|
38077
|
-
]);
|
|
38078
|
-
if (options.quiet !== true) {
|
|
38079
|
-
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
38080
|
-
}
|
|
38081
|
-
return;
|
|
39348
|
+
if (parts[0] === "handoffs" && parts[1] === "active" && parts[2] && parts[3]?.endsWith(".md")) {
|
|
39349
|
+
return { kind: "handoff", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
|
|
38082
39350
|
}
|
|
38083
|
-
|
|
38084
|
-
|
|
38085
|
-
|
|
38086
|
-
|
|
38087
|
-
|
|
38088
|
-
await refreshMemoryIndex(config2, ov, uri, options);
|
|
38089
|
-
} finally {
|
|
38090
|
-
await (0, import_promises4.rm)(stagingDir, { force: true, recursive: true });
|
|
39351
|
+
if (parts[0] === "durable" && parts[1] === "projects" && parts[2] && parts[3]?.endsWith(".md")) {
|
|
39352
|
+
return { kind: "durable", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
|
|
39353
|
+
}
|
|
39354
|
+
if (parts[0] === "incidents" && parts[1] === "active" && parts[2] && parts[3]?.endsWith(".md")) {
|
|
39355
|
+
return { kind: "incident", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
|
|
38091
39356
|
}
|
|
39357
|
+
return void 0;
|
|
38092
39358
|
}
|
|
38093
|
-
|
|
38094
|
-
|
|
38095
|
-
|
|
38096
|
-
const existedBeforeWrite = await vikingResourceExists(ov, config2, uri);
|
|
38097
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
38098
|
-
const existsNow = attempt === 0 ? existedBeforeWrite : await vikingResourceExists(ov, config2, uri);
|
|
38099
|
-
const ourWriteLanded = existsNow && !existedBeforeWrite;
|
|
38100
|
-
const mode = attempt === 0 ? initialMode : ourWriteLanded ? "replace" : initialMode;
|
|
38101
|
-
const args = withIdentity(config2, [
|
|
38102
|
-
"write",
|
|
38103
|
-
uri,
|
|
38104
|
-
"--from-file",
|
|
38105
|
-
fromFile,
|
|
38106
|
-
"--mode",
|
|
38107
|
-
mode,
|
|
38108
|
-
"--wait",
|
|
38109
|
-
"--timeout",
|
|
38110
|
-
"120"
|
|
38111
|
-
]);
|
|
38112
|
-
if (options.quiet !== true) {
|
|
38113
|
-
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
38114
|
-
}
|
|
38115
|
-
const result = await runCommand(ov, args, { allowFailure: true });
|
|
38116
|
-
if (result.exitCode === 0) {
|
|
38117
|
-
if (options.quiet !== true && result.stdout.trim()) {
|
|
38118
|
-
console.log(result.stdout.trim());
|
|
38119
|
-
}
|
|
38120
|
-
if (options.quiet !== true && result.stderr.trim()) {
|
|
38121
|
-
console.error(result.stderr.trim());
|
|
38122
|
-
}
|
|
38123
|
-
return;
|
|
38124
|
-
}
|
|
38125
|
-
if (isTransientOvFailure(result.stderr, result.stdout) && await vikingResourceExists(ov, config2, uri) && !existedBeforeWrite) {
|
|
38126
|
-
if (options.quiet !== true) {
|
|
38127
|
-
console.log(
|
|
38128
|
-
"OpenViking accepted the write but returned an error before the wait completed; draining the queue."
|
|
38129
|
-
);
|
|
38130
|
-
}
|
|
38131
|
-
await waitForOvQueue(ov, config2, options);
|
|
38132
|
-
return;
|
|
38133
|
-
}
|
|
38134
|
-
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
38135
|
-
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
38136
|
-
}
|
|
38137
|
-
if (isResourceBusyFailure(result.stderr, result.stdout)) {
|
|
38138
|
-
await waitForOvQueue(ov, config2, options);
|
|
38139
|
-
await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
|
|
38140
|
-
} else {
|
|
38141
|
-
await sleep(1e3 * (attempt + 1));
|
|
38142
|
-
}
|
|
39359
|
+
function groupableRecord(record2) {
|
|
39360
|
+
if (record2.metadata.status !== "active" || !isCompactableKind(record2.metadata.kind)) {
|
|
39361
|
+
return void 0;
|
|
38143
39362
|
}
|
|
39363
|
+
const project = normalizeOptionalMetadata(record2.metadata.project) ?? parseProjectFromUri(record2.uri);
|
|
39364
|
+
if (!project) {
|
|
39365
|
+
return void 0;
|
|
39366
|
+
}
|
|
39367
|
+
const topic = topicForRecord(record2);
|
|
39368
|
+
const groupKey = [record2.metadata.kind, project, topic ?? record2.uri].join("\0");
|
|
39369
|
+
return { groupKey, project, record: record2, topic };
|
|
38144
39370
|
}
|
|
38145
|
-
|
|
38146
|
-
|
|
38147
|
-
|
|
38148
|
-
|
|
38149
|
-
|
|
38150
|
-
);
|
|
38151
|
-
|
|
38152
|
-
|
|
38153
|
-
|
|
38154
|
-
|
|
38155
|
-
|
|
38156
|
-
|
|
38157
|
-
|
|
38158
|
-
|
|
39371
|
+
function topicForRecord(record2) {
|
|
39372
|
+
return normalizeOptionalMetadata(record2.metadata.topic) ?? normalizeOptionalMetadata(branchFromBody(record2.body)) ?? topicFromUri(record2.uri);
|
|
39373
|
+
}
|
|
39374
|
+
function branchFromBody(body) {
|
|
39375
|
+
const branch = /^branch:\s*(.+)$/m.exec(body)?.[1]?.trim();
|
|
39376
|
+
return branch?.split(/\s+/)[0]?.replace(/[.,;:]+$/g, "");
|
|
39377
|
+
}
|
|
39378
|
+
function topicFromUri(uri) {
|
|
39379
|
+
const name = (0, import_node_path4.basename)(uri).replace(/\.md$/, "");
|
|
39380
|
+
return name.startsWith("threadnote-") ? void 0 : name;
|
|
39381
|
+
}
|
|
39382
|
+
function parseProjectFromUri(uri) {
|
|
39383
|
+
const parts = uri.split("/memories/")[1]?.split("/").filter(Boolean) ?? [];
|
|
39384
|
+
if ((parts[0] === "handoffs" || parts[0] === "incidents") && parts[1] === "active") {
|
|
39385
|
+
return parts[2];
|
|
38159
39386
|
}
|
|
38160
|
-
if (
|
|
38161
|
-
|
|
38162
|
-
`Memory stored, but index refresh failed for ${uri}: ${result.stderr.trim() || result.stdout.trim()}`
|
|
38163
|
-
);
|
|
39387
|
+
if (parts[0] === "durable" && parts[1] === "projects") {
|
|
39388
|
+
return parts[2];
|
|
38164
39389
|
}
|
|
39390
|
+
return void 0;
|
|
38165
39391
|
}
|
|
38166
|
-
|
|
38167
|
-
|
|
38168
|
-
|
|
38169
|
-
|
|
39392
|
+
function comparableMemoryBody(body) {
|
|
39393
|
+
return stripHygieneSources(body).trim().replace(/\s+/g, " ");
|
|
39394
|
+
}
|
|
39395
|
+
function stripHygieneSources(body) {
|
|
39396
|
+
const index = body.indexOf(`
|
|
39397
|
+
${HYGIENE_SOURCES_HEADING}`);
|
|
39398
|
+
if (index !== -1) {
|
|
39399
|
+
return body.slice(0, index).trim();
|
|
39400
|
+
}
|
|
39401
|
+
return body.startsWith(HYGIENE_SOURCES_HEADING) ? "" : body.trim();
|
|
39402
|
+
}
|
|
39403
|
+
function preferredKeepRecord(records, topic) {
|
|
39404
|
+
const stableRecords = records.filter((record2) => isStableRecord(record2, topic));
|
|
39405
|
+
return sortedNewestFirst(stableRecords.length > 0 ? stableRecords : records)[0] ?? records[0];
|
|
39406
|
+
}
|
|
39407
|
+
function isStableRecord(record2, topic) {
|
|
39408
|
+
const recordTopic = topic ?? topicForRecord(record2);
|
|
39409
|
+
return recordTopic !== void 0 && (0, import_node_path4.basename)(record2.uri) === `${uriSegment(recordTopic)}.md`;
|
|
39410
|
+
}
|
|
39411
|
+
function sortedNewestFirst(records) {
|
|
39412
|
+
return [...records].sort((left, right) => {
|
|
39413
|
+
const timestampDiff = timestampMs(right) - timestampMs(left);
|
|
39414
|
+
return timestampDiff === 0 ? left.uri.localeCompare(right.uri) : timestampDiff;
|
|
39415
|
+
});
|
|
39416
|
+
}
|
|
39417
|
+
function timestampMs(record2) {
|
|
39418
|
+
const parsed = Date.parse(record2.metadata.timestamp);
|
|
39419
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
39420
|
+
}
|
|
39421
|
+
function isStaleLookingHandoff(record2, now) {
|
|
39422
|
+
if (record2.metadata.kind !== "handoff") {
|
|
39423
|
+
return false;
|
|
38170
39424
|
}
|
|
38171
|
-
if (
|
|
38172
|
-
|
|
39425
|
+
if (now.getTime() - timestampMs(record2) < STALE_HANDOFF_AGE_MS) {
|
|
39426
|
+
return false;
|
|
38173
39427
|
}
|
|
39428
|
+
return /\b(?:PR|pull request)\s+(?:OPEN|open|is open)|awaiting review|waiting for review|next steps?:\s*address PR review/i.test(
|
|
39429
|
+
record2.body
|
|
39430
|
+
);
|
|
38174
39431
|
}
|
|
38175
|
-
function
|
|
38176
|
-
const
|
|
38177
|
-
|
|
38178
|
-
|
|
38179
|
-
|
|
38180
|
-
|
|
38181
|
-
|
|
38182
|
-
${stdout2}`.toLowerCase();
|
|
38183
|
-
return output.includes("resource is busy") || output.includes("resource is being processed");
|
|
39432
|
+
function groupBy(values, keyForValue) {
|
|
39433
|
+
const groups = /* @__PURE__ */ new Map();
|
|
39434
|
+
for (const value of values) {
|
|
39435
|
+
const key = keyForValue(value);
|
|
39436
|
+
groups.set(key, [...groups.get(key) ?? [], value]);
|
|
39437
|
+
}
|
|
39438
|
+
return groups;
|
|
38184
39439
|
}
|
|
38185
|
-
|
|
38186
|
-
|
|
38187
|
-
await writeMemoryFile(config2, ov, uri, content, initialMode, false, options);
|
|
39440
|
+
function compareGroupedRecordLists(left, right) {
|
|
39441
|
+
return (left[0]?.groupKey ?? "").localeCompare(right[0]?.groupKey ?? "");
|
|
38188
39442
|
}
|
|
38189
|
-
|
|
38190
|
-
const
|
|
38191
|
-
|
|
38192
|
-
|
|
38193
|
-
|
|
38194
|
-
|
|
38195
|
-
return;
|
|
38196
|
-
}
|
|
38197
|
-
const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
|
|
38198
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
38199
|
-
const result = await runCommand(ov, args, { allowFailure: true });
|
|
38200
|
-
if (result.exitCode === 0) {
|
|
38201
|
-
if (options.quiet !== true && result.stdout.trim()) {
|
|
38202
|
-
console.log(result.stdout.trim());
|
|
38203
|
-
}
|
|
38204
|
-
return;
|
|
38205
|
-
}
|
|
38206
|
-
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
38207
|
-
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
38208
|
-
}
|
|
38209
|
-
if (isResourceBusyFailure(result.stderr, result.stdout)) {
|
|
38210
|
-
await waitForOvQueue(ov, config2, options);
|
|
38211
|
-
await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
|
|
38212
|
-
} else {
|
|
38213
|
-
await sleep(1e3 * (attempt + 1));
|
|
39443
|
+
function dedupeByUri(items) {
|
|
39444
|
+
const seen = /* @__PURE__ */ new Set();
|
|
39445
|
+
const result = [];
|
|
39446
|
+
for (const item of items) {
|
|
39447
|
+
if (seen.has(item.uri)) {
|
|
39448
|
+
continue;
|
|
38214
39449
|
}
|
|
39450
|
+
seen.add(item.uri);
|
|
39451
|
+
result.push(item);
|
|
38215
39452
|
}
|
|
39453
|
+
return result;
|
|
38216
39454
|
}
|
|
38217
|
-
|
|
38218
|
-
const
|
|
38219
|
-
|
|
39455
|
+
function formatMemoryDocument(title, metadata, body) {
|
|
39456
|
+
const header = [
|
|
39457
|
+
title,
|
|
39458
|
+
`kind: ${metadata.kind}`,
|
|
39459
|
+
`status: ${metadata.status}`,
|
|
39460
|
+
metadata.project ? `project: ${metadata.project}` : void 0,
|
|
39461
|
+
metadata.topic ? `topic: ${metadata.topic}` : void 0,
|
|
39462
|
+
`source_agent_client: ${metadata.sourceAgentClient}`,
|
|
39463
|
+
`timestamp: ${metadata.timestamp}`,
|
|
39464
|
+
metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
|
|
39465
|
+
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
|
|
39466
|
+
].filter((line) => line !== void 0);
|
|
39467
|
+
return [...header, "", body.trim()].join("\n");
|
|
38220
39468
|
}
|
|
38221
|
-
|
|
38222
|
-
const
|
|
38223
|
-
return
|
|
39469
|
+
function headerValue(header, key) {
|
|
39470
|
+
const prefix = `${key}:`;
|
|
39471
|
+
return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
|
|
38224
39472
|
}
|
|
38225
|
-
|
|
38226
|
-
if (
|
|
39473
|
+
function parseOptionalMemoryKind(value) {
|
|
39474
|
+
if (!value) {
|
|
38227
39475
|
return void 0;
|
|
38228
39476
|
}
|
|
38229
|
-
|
|
38230
|
-
|
|
38231
|
-
return void 0;
|
|
39477
|
+
if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
|
|
39478
|
+
return value;
|
|
38232
39479
|
}
|
|
38233
|
-
return
|
|
39480
|
+
return void 0;
|
|
38234
39481
|
}
|
|
38235
|
-
|
|
38236
|
-
|
|
38237
|
-
|
|
38238
|
-
});
|
|
38239
|
-
if (result.exitCode !== 0) {
|
|
38240
|
-
return [];
|
|
39482
|
+
function parseOptionalMemoryStatus(value) {
|
|
39483
|
+
if (!value) {
|
|
39484
|
+
return void 0;
|
|
38241
39485
|
}
|
|
38242
|
-
|
|
38243
|
-
|
|
38244
|
-
for (let index = 0; index < entries.length; ) {
|
|
38245
|
-
const raw = entries[index];
|
|
38246
|
-
const head = raw.slice(0, 1);
|
|
38247
|
-
if (head === "R" || head === "C") {
|
|
38248
|
-
const oldRel = entries[index + 1];
|
|
38249
|
-
const newRel = entries[index + 2];
|
|
38250
|
-
if (oldRel && newRel) {
|
|
38251
|
-
changes.push({ path: (0, import_node_path4.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
38252
|
-
changes.push({ path: (0, import_node_path4.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
38253
|
-
}
|
|
38254
|
-
index += 3;
|
|
38255
|
-
continue;
|
|
38256
|
-
}
|
|
38257
|
-
const rel = entries[index + 1];
|
|
38258
|
-
if (rel) {
|
|
38259
|
-
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
38260
|
-
changes.push({ path: (0, import_node_path4.join)(worktree, rel), relativePath: rel, status });
|
|
38261
|
-
}
|
|
38262
|
-
index += 2;
|
|
39486
|
+
if (["active", "archived", "superseded"].includes(value)) {
|
|
39487
|
+
return value;
|
|
38263
39488
|
}
|
|
38264
|
-
return
|
|
39489
|
+
return void 0;
|
|
38265
39490
|
}
|
|
38266
|
-
|
|
38267
|
-
const
|
|
38268
|
-
|
|
38269
|
-
for (const change of changes) {
|
|
38270
|
-
if (!change.relativePath.endsWith(".md")) {
|
|
38271
|
-
continue;
|
|
38272
|
-
}
|
|
38273
|
-
const firstSegment = change.relativePath.split("/")[0];
|
|
38274
|
-
if (!SHAREABLE_TOP_LEVEL_DIRS.includes(firstSegment)) {
|
|
38275
|
-
continue;
|
|
38276
|
-
}
|
|
38277
|
-
const uri = workfileToVikingUri(config2, team, change.path);
|
|
38278
|
-
try {
|
|
38279
|
-
if (change.status === "removed") {
|
|
38280
|
-
await removeMemoryUri(config2, ov, uri, false, options);
|
|
38281
|
-
continue;
|
|
38282
|
-
}
|
|
38283
|
-
if (!await isFile(change.path)) {
|
|
38284
|
-
continue;
|
|
38285
|
-
}
|
|
38286
|
-
const ovHasResource = await vikingResourceExists(ov, config2, uri);
|
|
38287
|
-
if (ovHasResource) {
|
|
38288
|
-
const reason = change.status === "modified" ? "overwriting local with upstream (local edits to the shared subtree are not preserved across sync)" : "aligning OV to upstream (resource pre-existed in OV, likely from an earlier local publish or sync)";
|
|
38289
|
-
if (options.quiet !== true) {
|
|
38290
|
-
console.warn(`share sync: ${uri}: ${reason}.`);
|
|
38291
|
-
}
|
|
38292
|
-
}
|
|
38293
|
-
await ensureSharedDirectoryChain(config2, ov, uri, false, options);
|
|
38294
|
-
const writeMode = ovHasResource ? "replace" : "create";
|
|
38295
|
-
await ingestSingleFile(ov, config2, uri, change.path, writeMode, options);
|
|
38296
|
-
} catch (err) {
|
|
38297
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
38298
|
-
if (options.quiet !== true) {
|
|
38299
|
-
console.warn(`share sync: ${uri}: ingest failed \u2014 will retry on the next sync. ${message}`);
|
|
38300
|
-
}
|
|
38301
|
-
failed.push(change);
|
|
38302
|
-
}
|
|
38303
|
-
}
|
|
38304
|
-
return { failed };
|
|
39491
|
+
function normalizeOptionalMetadata(value) {
|
|
39492
|
+
const trimmed = value?.trim();
|
|
39493
|
+
return trimmed ? trimmed : void 0;
|
|
38305
39494
|
}
|
|
38306
|
-
function
|
|
38307
|
-
|
|
38308
|
-
for (const list of lists) {
|
|
38309
|
-
for (const change of list) {
|
|
38310
|
-
map3.set(change.relativePath, change);
|
|
38311
|
-
}
|
|
38312
|
-
}
|
|
38313
|
-
return [...map3.values()];
|
|
39495
|
+
function isCompactableKind(kind) {
|
|
39496
|
+
return kind === "durable" || kind === "handoff" || kind === "incident";
|
|
38314
39497
|
}
|
|
38315
|
-
|
|
38316
|
-
|
|
38317
|
-
|
|
39498
|
+
function memoryKindPlural(kind) {
|
|
39499
|
+
switch (kind) {
|
|
39500
|
+
case "handoff":
|
|
39501
|
+
return "handoffs";
|
|
39502
|
+
case "incident":
|
|
39503
|
+
return "incidents";
|
|
39504
|
+
case "durable":
|
|
39505
|
+
return "durable memories";
|
|
39506
|
+
case "preference":
|
|
39507
|
+
return "preferences";
|
|
39508
|
+
case "smoke":
|
|
39509
|
+
return "smoke memories";
|
|
38318
39510
|
}
|
|
38319
|
-
|
|
38320
|
-
|
|
38321
|
-
|
|
38322
|
-
|
|
38323
|
-
|
|
38324
|
-
} else {
|
|
38325
|
-
state.pendingReindexes.delete(team.name);
|
|
39511
|
+
}
|
|
39512
|
+
function formatPlanSection(title, lines) {
|
|
39513
|
+
if (lines.length === 0) {
|
|
39514
|
+
return `${title}:
|
|
39515
|
+
- none`;
|
|
38326
39516
|
}
|
|
38327
|
-
|
|
38328
|
-
return result;
|
|
39517
|
+
return [`${title}:`, ...lines.map((line) => `- ${line}`)].join("\n");
|
|
38329
39518
|
}
|
|
38330
39519
|
|
|
38331
39520
|
// src/mcp_server.ts
|
|
@@ -38408,6 +39597,15 @@ function registerTools(server, config2) {
|
|
|
38408
39597
|
);
|
|
38409
39598
|
registerArchiveTool(server, config2, "archive", "Compatibility alias for archive_context.");
|
|
38410
39599
|
registerCompactTool(server, config2);
|
|
39600
|
+
server.registerTool(
|
|
39601
|
+
"threadnote_guide",
|
|
39602
|
+
{
|
|
39603
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
39604
|
+
description: "Onboard the user to Threadnote. Call this when the user asks what they can do with Threadnote, how to get started, for a feature tour/overview, or how Threadnote works \u2014 anything exploratory about Threadnote's capabilities. Returns a state-aware walkthrough (tailored to the user's server health, configured share teams, and seeded projects) written as guidance for YOU to present conversationally and offer to run step by step, not a message to paste. Prefer calling this over describing Threadnote from memory, so the guidance matches the user's actual setup. Takes no arguments.",
|
|
39605
|
+
inputSchema: {}
|
|
39606
|
+
},
|
|
39607
|
+
async () => runThreadnoteGuideTool(config2)
|
|
39608
|
+
);
|
|
38411
39609
|
server.registerTool(
|
|
38412
39610
|
"forget",
|
|
38413
39611
|
{
|
|
@@ -38457,14 +39655,14 @@ function registerTools(server, config2) {
|
|
|
38457
39655
|
"grep",
|
|
38458
39656
|
{
|
|
38459
39657
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
38460
|
-
description: "Run exact text search in OpenViking.",
|
|
39658
|
+
description: "Run exact text search in OpenViking. Defaults to your memories subtree when uri is omitted (OpenViking grep requires a scope).",
|
|
38461
39659
|
inputSchema: {
|
|
38462
39660
|
caseInsensitive: external_exports.boolean().optional().describe("Case-insensitive search"),
|
|
38463
39661
|
case_insensitive: external_exports.boolean().optional().describe("Case-insensitive search"),
|
|
38464
39662
|
nodeLimit: external_exports.number().int().positive().max(1e3).optional().describe("Maximum result count"),
|
|
38465
39663
|
node_limit: external_exports.number().int().positive().max(1e3).optional().describe("Maximum result count"),
|
|
38466
39664
|
pattern: external_exports.string().optional().describe("Required text or regex pattern"),
|
|
38467
|
-
uri: external_exports.string().optional().describe("Optional viking:// subtree")
|
|
39665
|
+
uri: external_exports.string().optional().describe("Optional viking:// subtree (defaults to your memories root)")
|
|
38468
39666
|
}
|
|
38469
39667
|
},
|
|
38470
39668
|
async ({ caseInsensitive, case_insensitive, nodeLimit, node_limit, pattern, uri }) => {
|
|
@@ -38480,7 +39678,7 @@ function registerTools(server, config2) {
|
|
|
38480
39678
|
case_insensitive: caseInsensitive ?? case_insensitive,
|
|
38481
39679
|
node_limit: nodeLimit ?? node_limit,
|
|
38482
39680
|
pattern: checkedPattern.value,
|
|
38483
|
-
uri: checkedUri.value
|
|
39681
|
+
uri: checkedUri.value ?? `viking://user/${uriSegment2(config2.user)}/memories`
|
|
38484
39682
|
});
|
|
38485
39683
|
}
|
|
38486
39684
|
);
|
|
@@ -38554,9 +39752,10 @@ function registerTools(server, config2) {
|
|
|
38554
39752
|
"share_skill",
|
|
38555
39753
|
{
|
|
38556
39754
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
38557
|
-
description: "Publish a local Codex/Claude skill or Claude command markdown file into a team's shared artifact catalog. Path inference handles ~/.codex/skills/**/SKILL.md, ~/.claude/skills/**/SKILL.md, and ~/.claude/commands/**/*.md; pass agent/kind/name when sharing from another path. Default team is used unless team is provided. Pass preview=true to inspect
|
|
39755
|
+
description: "Publish a local Codex/Claude skill or Claude command markdown file into a team's shared artifact catalog. Path inference handles ~/.codex/skills/**/SKILL.md, ~/.claude/skills/**/SKILL.md, and ~/.claude/commands/**/*.md; pass agent/kind/name when sharing from another path. A skill is shared as its whole directory: companion files (scripts, references, assets) beside the SKILL.md travel with it. Default team is used unless team is provided. Pass preview=true to inspect what would land without writing or committing.",
|
|
38558
39756
|
inputSchema: {
|
|
38559
39757
|
agent: external_exports.enum(["codex", "claude"]).optional().describe("Agent owner when path inference is ambiguous"),
|
|
39758
|
+
allowBinary: external_exports.boolean().optional().describe("Include binary skill files (unscannable by the scrubber); blocked by default"),
|
|
38560
39759
|
force: external_exports.boolean().optional().describe("Replace an existing shared artifact with different content"),
|
|
38561
39760
|
kind: external_exports.enum(["skill", "command"]).optional().describe("Artifact kind when path inference is ambiguous"),
|
|
38562
39761
|
message: external_exports.string().optional().describe('Commit message override; defaults to "share: publish <path>"'),
|
|
@@ -38568,7 +39767,7 @@ function registerTools(server, config2) {
|
|
|
38568
39767
|
team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
|
|
38569
39768
|
}
|
|
38570
39769
|
},
|
|
38571
|
-
async ({ agent, force, kind, message, name, path, preview, push, redact, team }) => {
|
|
39770
|
+
async ({ agent, allowBinary, force, kind, message, name, path, preview, push, redact, team }) => {
|
|
38572
39771
|
const checkedPath = requiredText(path, "share_skill", "path", {
|
|
38573
39772
|
path: "~/.codex/skills/example/SKILL.md"
|
|
38574
39773
|
});
|
|
@@ -38577,6 +39776,7 @@ function registerTools(server, config2) {
|
|
|
38577
39776
|
}
|
|
38578
39777
|
return runShareSkillTool(config2, checkedPath.value, {
|
|
38579
39778
|
agent,
|
|
39779
|
+
allowBinary,
|
|
38580
39780
|
force,
|
|
38581
39781
|
kind,
|
|
38582
39782
|
message,
|
|
@@ -38588,14 +39788,40 @@ function registerTools(server, config2) {
|
|
|
38588
39788
|
});
|
|
38589
39789
|
}
|
|
38590
39790
|
);
|
|
39791
|
+
server.registerTool(
|
|
39792
|
+
"share_bundle",
|
|
39793
|
+
{
|
|
39794
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
39795
|
+
description: "Publish a multi-skill constellation (pack) into a team's shared artifact catalog from a threadnote-bundle.json manifest. Use this when several skills share code that lives outside any single skill directory (e.g. repo-root scripts/lib). The manifest declares name, agent, skills, include paths, external deps, and pathRewrites. Hardcoded repo-root paths are rewritten to a portable token and expanded on install. Pass preview=true to inspect what would land without writing or committing.",
|
|
39796
|
+
inputSchema: {
|
|
39797
|
+
allowBinary: external_exports.boolean().optional().describe("Include binary files (unscannable by the scrubber); blocked by default"),
|
|
39798
|
+
force: external_exports.boolean().optional().describe("Replace existing shared pack files with different content"),
|
|
39799
|
+
message: external_exports.string().optional().describe("Commit message override"),
|
|
39800
|
+
path: external_exports.string().optional().describe("Required local path to a threadnote-bundle.json manifest"),
|
|
39801
|
+
preview: external_exports.boolean().optional().describe("Return what would land in the shared git repo without writing"),
|
|
39802
|
+
push: external_exports.boolean().optional().describe("Push to remote after committing; defaults to true"),
|
|
39803
|
+
redact: external_exports.boolean().optional().describe("Replace soft-leak matches (local paths) with placeholders and continue; credentials still block."),
|
|
39804
|
+
team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
|
|
39805
|
+
}
|
|
39806
|
+
},
|
|
39807
|
+
async ({ allowBinary, force, message, path, preview, push, redact, team }) => {
|
|
39808
|
+
const checkedPath = requiredText(path, "share_bundle", "path", {
|
|
39809
|
+
path: "~/src/reviewer/threadnote-bundle.json"
|
|
39810
|
+
});
|
|
39811
|
+
if (!checkedPath.ok) {
|
|
39812
|
+
return checkedPath.error;
|
|
39813
|
+
}
|
|
39814
|
+
return runShareBundleTool(config2, checkedPath.value, { allowBinary, force, message, preview, push, redact, team });
|
|
39815
|
+
}
|
|
39816
|
+
);
|
|
38591
39817
|
server.registerTool(
|
|
38592
39818
|
"list_shared_skills",
|
|
38593
39819
|
{
|
|
38594
39820
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
38595
|
-
description: "List shared Codex/Claude skills
|
|
39821
|
+
description: "List shared Codex/Claude skills, Claude commands, and skill packs available in a configured Threadnote team repo, including whether each one is already installed locally.",
|
|
38596
39822
|
inputSchema: {
|
|
38597
39823
|
agent: external_exports.enum(["codex", "claude"]).optional().describe("Optional agent filter"),
|
|
38598
|
-
kind: external_exports.enum(["skill", "command"]).optional().describe("Optional kind filter"),
|
|
39824
|
+
kind: external_exports.enum(["skill", "command", "pack"]).optional().describe("Optional kind filter"),
|
|
38599
39825
|
name: external_exports.string().optional().describe("Optional shared artifact name filter"),
|
|
38600
39826
|
team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
|
|
38601
39827
|
}
|
|
@@ -38611,7 +39837,7 @@ function registerTools(server, config2) {
|
|
|
38611
39837
|
agent: external_exports.enum(["codex", "claude"]).optional().describe("Agent owner; required when name is ambiguous"),
|
|
38612
39838
|
dryRun: external_exports.boolean().optional().describe("Preview install without writing local files"),
|
|
38613
39839
|
force: external_exports.boolean().optional().describe("Replace an existing installed artifact with different content"),
|
|
38614
|
-
kind: external_exports.enum(["skill", "command"]).optional().describe("Artifact kind; required when name is ambiguous"),
|
|
39840
|
+
kind: external_exports.enum(["skill", "command", "pack"]).optional().describe("Artifact kind; required when name is ambiguous"),
|
|
38615
39841
|
name: external_exports.string().optional().describe("Required shared artifact name to install"),
|
|
38616
39842
|
team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
|
|
38617
39843
|
}
|
|
@@ -39045,7 +40271,8 @@ async function runRecallTool(config2, params) {
|
|
|
39045
40271
|
passes.push(seeded.hits);
|
|
39046
40272
|
}
|
|
39047
40273
|
const sections = [];
|
|
39048
|
-
const
|
|
40274
|
+
const exactMatches = await collectExactMemoryMatches(config2, query, params.includeArchived, project);
|
|
40275
|
+
const { semanticSection, exactTail } = buildRecallSections(passes, exactMatches, params.nodeLimit ?? 12);
|
|
39049
40276
|
if (semanticSection) {
|
|
39050
40277
|
sections.push(semanticSection);
|
|
39051
40278
|
} else if (!base.ok) {
|
|
@@ -39054,9 +40281,8 @@ async function runRecallTool(config2, params) {
|
|
|
39054
40281
|
if (indexRepairMessages.length > 0) {
|
|
39055
40282
|
sections.push(indexRepairMessages.join("\n"));
|
|
39056
40283
|
}
|
|
39057
|
-
|
|
39058
|
-
|
|
39059
|
-
sections.push(exactMatches);
|
|
40284
|
+
if (exactTail) {
|
|
40285
|
+
sections.push(exactTail);
|
|
39060
40286
|
}
|
|
39061
40287
|
const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
|
|
39062
40288
|
if (hygieneHints) {
|
|
@@ -39103,14 +40329,14 @@ async function recallHygieneHintsSection(config2, recallText) {
|
|
|
39103
40329
|
const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
|
|
39104
40330
|
return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
|
|
39105
40331
|
}
|
|
39106
|
-
async function
|
|
40332
|
+
async function collectExactMemoryMatches(config2, query, includeArchived, project) {
|
|
39107
40333
|
const terms = exactRecallTerms(query);
|
|
39108
40334
|
if (terms.length === 0) {
|
|
39109
|
-
return
|
|
40335
|
+
return [];
|
|
39110
40336
|
}
|
|
39111
40337
|
const ov = await requiredOpenVikingCli2();
|
|
39112
40338
|
const scopes = exactMemoryScopes(config2, includeArchived, query, project);
|
|
39113
|
-
|
|
40339
|
+
return collectExactMatches(terms, scopes, async (term, scope) => {
|
|
39114
40340
|
const result = await runCommand(
|
|
39115
40341
|
ov,
|
|
39116
40342
|
withIdentity2(config2, ["grep", term, "--uri", scope, "--node-limit", "5", "--output", "json"]),
|
|
@@ -39118,7 +40344,6 @@ async function exactMemoryMatchesText(config2, query, includeArchived, project)
|
|
|
39118
40344
|
);
|
|
39119
40345
|
return result.exitCode === 0 ? result.stdout : void 0;
|
|
39120
40346
|
});
|
|
39121
|
-
return formatExactMatchPointers(matches);
|
|
39122
40347
|
}
|
|
39123
40348
|
function registerReadTool(server, config2, name, description) {
|
|
39124
40349
|
server.registerTool(
|
|
@@ -39998,6 +41223,34 @@ async function runShareSkillTool(config2, sourcePath, options) {
|
|
|
39998
41223
|
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
39999
41224
|
}
|
|
40000
41225
|
}
|
|
41226
|
+
async function runShareBundleTool(config2, manifestPath, options) {
|
|
41227
|
+
try {
|
|
41228
|
+
const result = await shareBundlePack(config2, manifestPath, options);
|
|
41229
|
+
const lines = [...result.messages, ...result.gitMessages];
|
|
41230
|
+
if (result.previewContent !== void 0) {
|
|
41231
|
+
lines.push("-----BEGIN PREVIEW-----");
|
|
41232
|
+
lines.push(result.previewContent);
|
|
41233
|
+
lines.push("-----END PREVIEW-----");
|
|
41234
|
+
}
|
|
41235
|
+
return { content: [{ type: "text", text: lines.join("\n") }], isError: false };
|
|
41236
|
+
} catch (err) {
|
|
41237
|
+
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
41238
|
+
}
|
|
41239
|
+
}
|
|
41240
|
+
async function runThreadnoteGuideTool(config2) {
|
|
41241
|
+
const serverUp = await probeServerUp(config2);
|
|
41242
|
+
const context = await gatherOnboardingContext(config2);
|
|
41243
|
+
const text = buildOnboardingGuide({ ...context, serverUp });
|
|
41244
|
+
return { content: [{ type: "text", text }], isError: false };
|
|
41245
|
+
}
|
|
41246
|
+
async function probeServerUp(config2) {
|
|
41247
|
+
try {
|
|
41248
|
+
const result = await runOpenVikingMcpTool(config2, "health", {});
|
|
41249
|
+
return result.isError !== true;
|
|
41250
|
+
} catch (_err) {
|
|
41251
|
+
return false;
|
|
41252
|
+
}
|
|
41253
|
+
}
|
|
40001
41254
|
async function runListSharedSkillsTool(config2, options) {
|
|
40002
41255
|
try {
|
|
40003
41256
|
const result = await listSharedAgentArtifacts(config2, options);
|