threadnote 1.2.0 → 1.3.1
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/README.md +8 -0
- package/dist/mcp_server.cjs +2175 -1022
- package/dist/threadnote.cjs +1076 -18
- package/docs/agent-instructions.md +6 -0
- package/docs/index.html +11 -0
- package/docs/share.md +102 -0
- package/package.json +1 -1
package/dist/mcp_server.cjs
CHANGED
|
@@ -36820,835 +36820,1126 @@ function indexRepairWarning(uri, result) {
|
|
|
36820
36820
|
return `${uri}: ${result.stderr.trim() || result.stdout.trim() || "reindex failed"}`;
|
|
36821
36821
|
}
|
|
36822
36822
|
|
|
36823
|
-
// src/
|
|
36823
|
+
// src/share.ts
|
|
36824
|
+
var import_promises4 = require("node:fs/promises");
|
|
36825
|
+
var import_node_os2 = require("node:os");
|
|
36824
36826
|
var import_node_path3 = require("node:path");
|
|
36825
|
-
var
|
|
36826
|
-
var
|
|
36827
|
-
|
|
36828
|
-
|
|
36829
|
-
|
|
36830
|
-
|
|
36831
|
-
|
|
36832
|
-
|
|
36833
|
-
|
|
36834
|
-
|
|
36835
|
-
|
|
36836
|
-
|
|
36837
|
-
|
|
36838
|
-
|
|
36839
|
-
|
|
36840
|
-
|
|
36841
|
-
|
|
36842
|
-
|
|
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 });
|
|
36843
36889
|
}
|
|
36844
|
-
return {
|
|
36845
|
-
body,
|
|
36846
|
-
content: trimmed,
|
|
36847
|
-
headerTitle: firstLine,
|
|
36848
|
-
metadata: {
|
|
36849
|
-
archivedFrom: headerValue(header, "archived_from"),
|
|
36850
|
-
kind,
|
|
36851
|
-
project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
|
|
36852
|
-
sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
|
|
36853
|
-
status,
|
|
36854
|
-
supersedes: headerValue(header, "supersedes"),
|
|
36855
|
-
timestamp: headerValue(header, "timestamp") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
36856
|
-
topic: normalizeOptionalMetadata(headerValue(header, "topic"))
|
|
36857
|
-
},
|
|
36858
|
-
uri
|
|
36859
|
-
};
|
|
36890
|
+
return { cleaned, redactions };
|
|
36860
36891
|
}
|
|
36861
|
-
|
|
36862
|
-
|
|
36863
|
-
const
|
|
36864
|
-
|
|
36865
|
-
|
|
36866
|
-
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;
|
|
36867
36897
|
}
|
|
36868
|
-
|
|
36869
|
-
|
|
36870
|
-
|
|
36871
|
-
|
|
36872
|
-
|
|
36873
|
-
|
|
36874
|
-
|
|
36875
|
-
|
|
36876
|
-
|
|
36877
|
-
|
|
36878
|
-
|
|
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 };
|
|
36879
36916
|
}
|
|
36880
|
-
const
|
|
36881
|
-
const
|
|
36882
|
-
const
|
|
36883
|
-
|
|
36884
|
-
|
|
36885
|
-
|
|
36886
|
-
|
|
36887
|
-
|
|
36888
|
-
|
|
36889
|
-
|
|
36890
|
-
|
|
36891
|
-
}
|
|
36892
|
-
|
|
36893
|
-
|
|
36894
|
-
|
|
36895
|
-
duplicateKeep,
|
|
36896
|
-
duplicateGroup.map((record2) => record2.uri)
|
|
36897
|
-
),
|
|
36898
|
-
reason: "keep exact duplicate group with source URIs",
|
|
36899
|
-
sourceUris: duplicateGroup.map((record2) => record2.uri),
|
|
36900
|
-
uri: duplicateKeep.uri
|
|
36901
|
-
});
|
|
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
|
+
);
|
|
36902
36932
|
}
|
|
36903
36933
|
}
|
|
36904
|
-
|
|
36905
|
-
|
|
36906
|
-
|
|
36907
|
-
|
|
36908
|
-
|
|
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)) {
|
|
36909
36965
|
continue;
|
|
36910
36966
|
}
|
|
36911
|
-
|
|
36912
|
-
|
|
36913
|
-
if (
|
|
36967
|
+
const changes = [];
|
|
36968
|
+
for (const item of value) {
|
|
36969
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
36914
36970
|
continue;
|
|
36915
36971
|
}
|
|
36916
|
-
|
|
36917
|
-
|
|
36918
|
-
|
|
36919
|
-
reason: "strip self-supersedes header",
|
|
36920
|
-
sourceUris: [record2.uri],
|
|
36921
|
-
uri: record2.uri
|
|
36922
|
-
});
|
|
36923
|
-
}
|
|
36924
|
-
if (isStaleLookingHandoff(record2, now)) {
|
|
36925
|
-
manualReview.push({ reason: "stale-looking active handoff", uri: record2.uri });
|
|
36926
|
-
}
|
|
36927
|
-
continue;
|
|
36928
|
-
}
|
|
36929
|
-
if (kind === "handoff") {
|
|
36930
|
-
const keep = preferredKeepRecord(remainingRecords, topic);
|
|
36931
|
-
const sourceUris = recordsInGroup.map((record2) => record2.uri);
|
|
36932
|
-
keepUpdates.push({
|
|
36933
|
-
content: memoryContentWithHygieneSources(keep, sourceUris),
|
|
36934
|
-
reason: "keep latest handoff and preserve source URIs",
|
|
36935
|
-
sourceUris,
|
|
36936
|
-
uri: keep.uri
|
|
36937
|
-
});
|
|
36938
|
-
for (const record2 of sortedNewestFirst(remainingRecords).filter((item) => item.uri !== keep.uri)) {
|
|
36939
|
-
archives.push({
|
|
36940
|
-
kind,
|
|
36941
|
-
project,
|
|
36942
|
-
reason: `older handoff for ${project}/${topic ?? "unknown"}`,
|
|
36943
|
-
topic,
|
|
36944
|
-
uri: record2.uri
|
|
36945
|
-
});
|
|
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 });
|
|
36946
36975
|
}
|
|
36947
|
-
continue;
|
|
36948
36976
|
}
|
|
36949
|
-
|
|
36950
|
-
|
|
36977
|
+
if (changes.length > 0) {
|
|
36978
|
+
pending.set(team, changes);
|
|
36951
36979
|
}
|
|
36952
36980
|
}
|
|
36953
|
-
|
|
36954
|
-
archives: dedupeByUri(archives),
|
|
36955
|
-
forgets: dedupeByUri(forgets),
|
|
36956
|
-
keepUpdates: dedupeByUri(keepUpdates),
|
|
36957
|
-
manualReview: dedupeByUri(manualReview),
|
|
36958
|
-
options,
|
|
36959
|
-
recordsScanned: groupedRecords.length
|
|
36960
|
-
};
|
|
36981
|
+
state.pendingReindexes = pending;
|
|
36961
36982
|
}
|
|
36962
|
-
function
|
|
36963
|
-
const
|
|
36964
|
-
|
|
36965
|
-
|
|
36966
|
-
|
|
36967
|
-
|
|
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
|
|
36968
36992
|
};
|
|
36969
|
-
|
|
36970
|
-
|
|
36971
|
-
|
|
36972
|
-
|
|
36973
|
-
);
|
|
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);
|
|
36974
36998
|
}
|
|
36975
|
-
function
|
|
36976
|
-
const
|
|
36977
|
-
|
|
36978
|
-
|
|
36979
|
-
|
|
36980
|
-
|
|
36981
|
-
const
|
|
36982
|
-
|
|
36983
|
-
`Records scanned: ${plan.recordsScanned}`,
|
|
36984
|
-
"",
|
|
36985
|
-
formatPlanSection(
|
|
36986
|
-
"Keep/update",
|
|
36987
|
-
plan.keepUpdates.map((action) => `${action.uri} (${action.reason}; sources: ${action.sourceUris.length})`)
|
|
36988
|
-
),
|
|
36989
|
-
formatPlanSection(
|
|
36990
|
-
"Archive old handoffs",
|
|
36991
|
-
plan.archives.map((action) => `${action.uri} (${action.reason})`)
|
|
36992
|
-
),
|
|
36993
|
-
formatPlanSection(
|
|
36994
|
-
"Forget exact duplicates",
|
|
36995
|
-
plan.forgets.map((action) => `${action.uri} (${action.reason})`)
|
|
36996
|
-
),
|
|
36997
|
-
formatPlanSection(
|
|
36998
|
-
"Manual review",
|
|
36999
|
-
plan.manualReview.map((item) => `${item.uri} (${item.reason})`)
|
|
37000
|
-
)
|
|
37001
|
-
];
|
|
37002
|
-
if (!options.apply) {
|
|
37003
|
-
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);
|
|
37004
37007
|
}
|
|
37005
|
-
return lines.join("\n");
|
|
37006
37008
|
}
|
|
37007
|
-
function
|
|
37008
|
-
const
|
|
37009
|
-
|
|
37010
|
-
|
|
37011
|
-
const returnedRecords = options.records?.filter((record2) => returnedUriSet.has(record2.uri)).map((record2) => groupableRecord(record2)) ?? [];
|
|
37012
|
-
const groups = /* @__PURE__ */ new Map();
|
|
37013
|
-
for (const item of returnedRecords) {
|
|
37014
|
-
if (!item) {
|
|
37015
|
-
continue;
|
|
37016
|
-
}
|
|
37017
|
-
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 [];
|
|
37018
37013
|
}
|
|
37019
|
-
|
|
37020
|
-
|
|
37021
|
-
|
|
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
|
+
}
|
|
37022
37026
|
}
|
|
37023
|
-
|
|
37024
|
-
|
|
37027
|
+
state.behindTeams = nextBehindTeams;
|
|
37028
|
+
return statuses.flatMap((status) => status.warning ? [status.warning] : []);
|
|
37029
|
+
} finally {
|
|
37030
|
+
state.lastCheckedAt = Date.now();
|
|
37031
|
+
}
|
|
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 [];
|
|
37044
|
+
}
|
|
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
|
+
});
|
|
37025
37057
|
continue;
|
|
37026
37058
|
}
|
|
37027
|
-
const
|
|
37028
|
-
if (
|
|
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
|
+
});
|
|
37029
37066
|
continue;
|
|
37030
37067
|
}
|
|
37031
|
-
|
|
37032
|
-
`${group.length} active ${memoryKindPlural(first.record.metadata.kind)} look overlapping for ${first.project}/${topic}; run compact_context({"project":"${first.project}","topic":"${topic}","dryRun":true}).`
|
|
37033
|
-
);
|
|
37068
|
+
statuses.push({ behind: Number.parseInt(behind, 10) || 0, team: name });
|
|
37034
37069
|
}
|
|
37035
|
-
|
|
37036
|
-
|
|
37037
|
-
|
|
37038
|
-
|
|
37039
|
-
|
|
37040
|
-
|
|
37041
|
-
|
|
37070
|
+
return statuses;
|
|
37071
|
+
}
|
|
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 });
|
|
37042
37079
|
}
|
|
37043
|
-
|
|
37044
|
-
|
|
37045
|
-
|
|
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.`;
|
|
37082
|
+
}
|
|
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.`;
|
|
37086
|
+
}
|
|
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.`;
|
|
37089
|
+
}
|
|
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
|
+
);
|
|
37046
37097
|
}
|
|
37047
|
-
|
|
37048
|
-
`
|
|
37098
|
+
throw new Error(
|
|
37099
|
+
`Automatic share sync failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
|
|
37049
37100
|
);
|
|
37050
37101
|
}
|
|
37051
|
-
|
|
37052
|
-
|
|
37053
|
-
|
|
37054
|
-
|
|
37055
|
-
|
|
37056
|
-
|
|
37057
|
-
|
|
37058
|
-
const uri = match[0]?.replace(/[.,;:]+$/g, "");
|
|
37059
|
-
if (!uri || !parsePersonalMemoryUri(uri, userSegment)) {
|
|
37060
|
-
continue;
|
|
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 });
|
|
37061
37109
|
}
|
|
37062
|
-
uris.push(uri);
|
|
37063
37110
|
}
|
|
37064
|
-
return
|
|
37111
|
+
return void 0;
|
|
37065
37112
|
}
|
|
37066
|
-
function
|
|
37067
|
-
const
|
|
37068
|
-
|
|
37069
|
-
|
|
37070
|
-
|
|
37071
|
-
const
|
|
37072
|
-
const
|
|
37073
|
-
|
|
37074
|
-
|
|
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"}`);
|
|
37075
37124
|
}
|
|
37076
|
-
if (
|
|
37077
|
-
|
|
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
|
+
}
|
|
37078
37139
|
}
|
|
37079
|
-
if (
|
|
37080
|
-
|
|
37140
|
+
if (!push) {
|
|
37141
|
+
messages.push("git push skipped (push=false)");
|
|
37142
|
+
return messages;
|
|
37081
37143
|
}
|
|
37082
|
-
|
|
37083
|
-
|
|
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"}`);
|
|
37084
37152
|
}
|
|
37085
|
-
return
|
|
37153
|
+
return messages;
|
|
37086
37154
|
}
|
|
37087
|
-
function
|
|
37088
|
-
if (
|
|
37155
|
+
async function runGitCommand(dryRun, git, args, failureLabel) {
|
|
37156
|
+
if (dryRun) {
|
|
37157
|
+
console.log(`Would run: ${formatShellCommand(git, args)}`);
|
|
37089
37158
|
return void 0;
|
|
37090
37159
|
}
|
|
37091
|
-
const
|
|
37092
|
-
if (
|
|
37093
|
-
|
|
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"}`);
|
|
37094
37163
|
}
|
|
37095
|
-
|
|
37096
|
-
const groupKey = [record2.metadata.kind, project, topic ?? record2.uri].join("\0");
|
|
37097
|
-
return { groupKey, project, record: record2, topic };
|
|
37098
|
-
}
|
|
37099
|
-
function topicForRecord(record2) {
|
|
37100
|
-
return normalizeOptionalMetadata(record2.metadata.topic) ?? normalizeOptionalMetadata(branchFromBody(record2.body)) ?? topicFromUri(record2.uri);
|
|
37101
|
-
}
|
|
37102
|
-
function branchFromBody(body) {
|
|
37103
|
-
const branch = /^branch:\s*(.+)$/m.exec(body)?.[1]?.trim();
|
|
37104
|
-
return branch?.split(/\s+/)[0]?.replace(/[.,;:]+$/g, "");
|
|
37105
|
-
}
|
|
37106
|
-
function topicFromUri(uri) {
|
|
37107
|
-
const name = (0, import_node_path3.basename)(uri).replace(/\.md$/, "");
|
|
37108
|
-
return name.startsWith("threadnote-") ? void 0 : name;
|
|
37164
|
+
return result;
|
|
37109
37165
|
}
|
|
37110
|
-
function
|
|
37111
|
-
const
|
|
37112
|
-
|
|
37113
|
-
|
|
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}`);
|
|
37114
37171
|
}
|
|
37115
|
-
|
|
37116
|
-
|
|
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
|
+
}
|
|
37117
37179
|
}
|
|
37118
|
-
return
|
|
37119
|
-
}
|
|
37120
|
-
function comparableMemoryBody(body) {
|
|
37121
|
-
return stripHygieneSources(body).trim().replace(/\s+/g, " ");
|
|
37180
|
+
return shareSingleArtifact(config2, team, resolvedSourcePath, artifact, options);
|
|
37122
37181
|
}
|
|
37123
|
-
function
|
|
37124
|
-
const
|
|
37125
|
-
|
|
37126
|
-
|
|
37127
|
-
|
|
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}`);
|
|
37128
37188
|
}
|
|
37129
|
-
|
|
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
|
+
};
|
|
37222
|
+
}
|
|
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 };
|
|
37130
37251
|
}
|
|
37131
|
-
function
|
|
37132
|
-
const
|
|
37133
|
-
|
|
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
|
+
};
|
|
37134
37367
|
}
|
|
37135
|
-
function
|
|
37136
|
-
const
|
|
37137
|
-
|
|
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
|
+
};
|
|
37385
|
+
}
|
|
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
|
+
};
|
|
37138
37397
|
}
|
|
37139
|
-
function
|
|
37140
|
-
return [...
|
|
37141
|
-
|
|
37142
|
-
|
|
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);
|
|
37143
37407
|
});
|
|
37144
37408
|
}
|
|
37145
|
-
function
|
|
37146
|
-
const
|
|
37147
|
-
|
|
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
|
+
`;
|
|
37148
37417
|
}
|
|
37149
|
-
function
|
|
37150
|
-
|
|
37151
|
-
|
|
37152
|
-
|
|
37153
|
-
|
|
37154
|
-
|
|
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
|
+
}
|
|
37155
37438
|
}
|
|
37156
|
-
|
|
37157
|
-
|
|
37158
|
-
);
|
|
37439
|
+
await visit(skillDir);
|
|
37440
|
+
return out.sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
37159
37441
|
}
|
|
37160
|
-
function
|
|
37161
|
-
|
|
37162
|
-
|
|
37163
|
-
const key = keyForValue(value);
|
|
37164
|
-
groups.set(key, [...groups.get(key) ?? [], value]);
|
|
37442
|
+
function isIgnoredBundleFile(name) {
|
|
37443
|
+
if (name === ".DS_Store" || name === BUNDLE_MANIFEST_FILE || name === BUNDLE_INSTALL_METADATA_FILE) {
|
|
37444
|
+
return true;
|
|
37165
37445
|
}
|
|
37166
|
-
|
|
37446
|
+
if (OV_SUMMARY_FILES.includes(name)) {
|
|
37447
|
+
return true;
|
|
37448
|
+
}
|
|
37449
|
+
return name.endsWith(".log") || name.endsWith(".threadnote-install.json");
|
|
37167
37450
|
}
|
|
37168
|
-
function
|
|
37169
|
-
|
|
37451
|
+
function isProbablyBinary(buffer) {
|
|
37452
|
+
if (buffer.includes(0)) {
|
|
37453
|
+
return true;
|
|
37454
|
+
}
|
|
37455
|
+
try {
|
|
37456
|
+
new import_node_util.TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
37457
|
+
return false;
|
|
37458
|
+
} catch (_err) {
|
|
37459
|
+
return true;
|
|
37460
|
+
}
|
|
37170
37461
|
}
|
|
37171
|
-
function
|
|
37172
|
-
const
|
|
37173
|
-
const
|
|
37174
|
-
|
|
37175
|
-
|
|
37176
|
-
continue;
|
|
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;
|
|
37177
37467
|
}
|
|
37178
|
-
seen.add(item.uri);
|
|
37179
|
-
result.push(item);
|
|
37180
37468
|
}
|
|
37181
|
-
return
|
|
37182
|
-
}
|
|
37183
|
-
function formatMemoryDocument(title, metadata, body) {
|
|
37184
|
-
const header = [
|
|
37185
|
-
title,
|
|
37186
|
-
`kind: ${metadata.kind}`,
|
|
37187
|
-
`status: ${metadata.status}`,
|
|
37188
|
-
metadata.project ? `project: ${metadata.project}` : void 0,
|
|
37189
|
-
metadata.topic ? `topic: ${metadata.topic}` : void 0,
|
|
37190
|
-
`source_agent_client: ${metadata.sourceAgentClient}`,
|
|
37191
|
-
`timestamp: ${metadata.timestamp}`,
|
|
37192
|
-
metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
|
|
37193
|
-
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
|
|
37194
|
-
].filter((line) => line !== void 0);
|
|
37195
|
-
return [...header, "", body.trim()].join("\n");
|
|
37196
|
-
}
|
|
37197
|
-
function headerValue(header, key) {
|
|
37198
|
-
const prefix = `${key}:`;
|
|
37199
|
-
return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
|
|
37469
|
+
return void 0;
|
|
37200
37470
|
}
|
|
37201
|
-
function
|
|
37202
|
-
|
|
37203
|
-
|
|
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";
|
|
37476
|
+
}
|
|
37204
37477
|
}
|
|
37205
|
-
|
|
37206
|
-
|
|
37478
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
37479
|
+
if (pattern.placeholder !== void 0 && pattern.regex.test(latin1)) {
|
|
37480
|
+
return pattern.name;
|
|
37481
|
+
}
|
|
37207
37482
|
}
|
|
37208
37483
|
return void 0;
|
|
37209
37484
|
}
|
|
37210
|
-
function
|
|
37211
|
-
|
|
37485
|
+
async function readFileBytesIfExists(path) {
|
|
37486
|
+
try {
|
|
37487
|
+
return await (0, import_promises4.readFile)(path);
|
|
37488
|
+
} catch (_err) {
|
|
37212
37489
|
return void 0;
|
|
37213
37490
|
}
|
|
37214
|
-
|
|
37215
|
-
|
|
37491
|
+
}
|
|
37492
|
+
function isBundleArtifact(artifact) {
|
|
37493
|
+
if (artifact.artifact.kind === "pack") {
|
|
37494
|
+
return true;
|
|
37216
37495
|
}
|
|
37217
|
-
return void 0;
|
|
37496
|
+
return artifact.members !== void 0 && artifact.members.length > 1;
|
|
37218
37497
|
}
|
|
37219
|
-
function
|
|
37220
|
-
|
|
37221
|
-
return trimmed ? trimmed : void 0;
|
|
37222
|
-
}
|
|
37223
|
-
function isCompactableKind(kind) {
|
|
37224
|
-
return kind === "durable" || kind === "handoff" || kind === "incident";
|
|
37498
|
+
function compareStrings(a, b) {
|
|
37499
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
37225
37500
|
}
|
|
37226
|
-
function
|
|
37227
|
-
|
|
37228
|
-
|
|
37229
|
-
|
|
37230
|
-
case "incident":
|
|
37231
|
-
return "incidents";
|
|
37232
|
-
case "durable":
|
|
37233
|
-
return "durable memories";
|
|
37234
|
-
case "preference":
|
|
37235
|
-
return "preferences";
|
|
37236
|
-
case "smoke":
|
|
37237
|
-
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}`);
|
|
37238
37505
|
}
|
|
37239
|
-
|
|
37240
|
-
|
|
37241
|
-
|
|
37242
|
-
return `${title}:
|
|
37243
|
-
- 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}`);
|
|
37244
37509
|
}
|
|
37245
|
-
|
|
37246
|
-
|
|
37247
|
-
|
|
37248
|
-
// src/share.ts
|
|
37249
|
-
var import_promises4 = require("node:fs/promises");
|
|
37250
|
-
var import_node_os2 = require("node:os");
|
|
37251
|
-
var import_node_path4 = require("node:path");
|
|
37252
|
-
var TEAMS_FILE_VERSION = 1;
|
|
37253
|
-
var SHARED_SEGMENT = "shared";
|
|
37254
|
-
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
37255
|
-
var SHAREABLE_ARTIFACT_DIR = "agent-artifacts";
|
|
37256
|
-
var SHAREABLE_TOP_LEVEL_DIRS = [...SHAREABLE_MEMORY_KIND_DIRS, SHAREABLE_ARTIFACT_DIR];
|
|
37257
|
-
var ARTIFACT_INSTALL_METADATA_VERSION = 1;
|
|
37258
|
-
var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
37259
|
-
var DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
37260
|
-
var SCRUBBER_PATTERNS = [
|
|
37261
|
-
// Credentials: never redactable. Blocking is the only safe response —
|
|
37262
|
-
// automated redaction risks false negatives that leave material in git.
|
|
37263
|
-
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
37264
|
-
{ name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
37265
|
-
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
37266
|
-
{ name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
|
|
37267
|
-
{ name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
|
|
37268
|
-
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
37269
|
-
// Matches bare JWTs (three base64url segments). May surface a JWE token in
|
|
37270
|
-
// legitimate docs; if that becomes noisy we can switch to warn-only.
|
|
37271
|
-
{ name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
37272
|
-
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
37273
|
-
// Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
|
|
37274
|
-
// xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
|
|
37275
|
-
{ name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i },
|
|
37276
|
-
// Soft leaks: block by default (so the agent sees them and decides), but
|
|
37277
|
-
// allow opt-in redaction so curated memories with incidental matches can
|
|
37278
|
-
// ship without a manual rewrite. Local home paths are the recurring
|
|
37279
|
-
// real-world leak; the regexes greedily consume the whole path segment
|
|
37280
|
-
// (including subdirectories) up to whitespace or common closing punctuation
|
|
37281
|
-
// so redaction collapses an entire path to a single placeholder rather than
|
|
37282
|
-
// leaving the subpath visible.
|
|
37283
|
-
{ name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
|
|
37284
|
-
{ name: "linux home path", placeholder: "<local-path>", regex: /\b\/home\/[^\s)>"'`,]+/ }
|
|
37285
|
-
];
|
|
37286
|
-
function applyScrubber(content, { redact }) {
|
|
37287
|
-
let cleaned = content;
|
|
37288
|
-
const redactions = [];
|
|
37289
|
-
for (const pattern of SCRUBBER_PATTERNS) {
|
|
37290
|
-
if (!pattern.regex.test(cleaned)) {
|
|
37291
|
-
continue;
|
|
37292
|
-
}
|
|
37293
|
-
if (!pattern.placeholder || !redact) {
|
|
37294
|
-
return { blocker: pattern.name, cleaned: content, redactions: [] };
|
|
37295
|
-
}
|
|
37296
|
-
const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`;
|
|
37297
|
-
const globalRegex = new RegExp(pattern.regex.source, flags);
|
|
37298
|
-
const matches = cleaned.match(globalRegex) ?? [];
|
|
37299
|
-
cleaned = cleaned.replace(globalRegex, pattern.placeholder);
|
|
37300
|
-
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}`);
|
|
37301
37513
|
}
|
|
37302
|
-
|
|
37303
|
-
|
|
37304
|
-
|
|
37305
|
-
|
|
37306
|
-
const state = autoShareState(config2);
|
|
37307
|
-
if (state.timer) {
|
|
37308
|
-
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}`);
|
|
37309
37518
|
}
|
|
37310
|
-
|
|
37311
|
-
|
|
37312
|
-
|
|
37313
|
-
|
|
37314
|
-
|
|
37315
|
-
|
|
37316
|
-
|
|
37317
|
-
|
|
37318
|
-
|
|
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
|
+
};
|
|
37319
37542
|
}
|
|
37320
|
-
async function
|
|
37321
|
-
const
|
|
37322
|
-
|
|
37323
|
-
|
|
37324
|
-
|
|
37325
|
-
|
|
37326
|
-
if (syncTeams.size === 0) {
|
|
37327
|
-
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}").`);
|
|
37328
37549
|
}
|
|
37329
|
-
const
|
|
37330
|
-
|
|
37331
|
-
|
|
37332
|
-
|
|
37333
|
-
|
|
37334
|
-
|
|
37335
|
-
|
|
37336
|
-
|
|
37337
|
-
remainingBehind.delete(team);
|
|
37338
|
-
syncedTeams.push(team);
|
|
37339
|
-
}
|
|
37340
|
-
} catch (err) {
|
|
37341
|
-
warnings.push(
|
|
37342
|
-
`Auto-sync for shared team "${team}" failed: ${err instanceof Error ? err.message : String(err)}`
|
|
37343
|
-
);
|
|
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 });
|
|
37344
37558
|
}
|
|
37559
|
+
return;
|
|
37345
37560
|
}
|
|
37346
|
-
|
|
37347
|
-
|
|
37348
|
-
|
|
37349
|
-
|
|
37350
|
-
}
|
|
37351
|
-
|
|
37352
|
-
const
|
|
37353
|
-
|
|
37354
|
-
|
|
37355
|
-
|
|
37356
|
-
|
|
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);
|
|
37357
37574
|
}
|
|
37358
|
-
|
|
37575
|
+
for (const include of manifest.include) {
|
|
37576
|
+
await addEntry(include);
|
|
37577
|
+
}
|
|
37578
|
+
return [...members.values()].sort((a, b) => compareStrings(a.relativePath, b.relativePath));
|
|
37359
37579
|
}
|
|
37360
|
-
function
|
|
37361
|
-
return (
|
|
37580
|
+
function escapeRegExp(value) {
|
|
37581
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37362
37582
|
}
|
|
37363
|
-
|
|
37364
|
-
|
|
37365
|
-
|
|
37366
|
-
|
|
37367
|
-
|
|
37368
|
-
|
|
37369
|
-
|
|
37370
|
-
|
|
37371
|
-
state.pendingReindexes = /* @__PURE__ */ new Map();
|
|
37372
|
-
return;
|
|
37373
|
-
}
|
|
37374
|
-
const pending = /* @__PURE__ */ new Map();
|
|
37375
|
-
for (const [team, value] of Object.entries(parsed.teams)) {
|
|
37376
|
-
if (!Array.isArray(value)) {
|
|
37377
|
-
continue;
|
|
37378
|
-
}
|
|
37379
|
-
const changes = [];
|
|
37380
|
-
for (const item of value) {
|
|
37381
|
-
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
37382
|
-
continue;
|
|
37383
|
-
}
|
|
37384
|
-
const entry = item;
|
|
37385
|
-
if (typeof entry.path === "string" && typeof entry.relativePath === "string" && (entry.status === "added" || entry.status === "removed" || entry.status === "modified")) {
|
|
37386
|
-
changes.push({ path: entry.path, relativePath: entry.relativePath, status: entry.status });
|
|
37387
|
-
}
|
|
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
|
+
);
|
|
37388
37591
|
}
|
|
37389
|
-
|
|
37390
|
-
|
|
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);
|
|
37391
37623
|
}
|
|
37392
37624
|
}
|
|
37393
|
-
|
|
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));
|
|
37394
37629
|
}
|
|
37395
|
-
async function
|
|
37396
|
-
const
|
|
37397
|
-
|
|
37398
|
-
|
|
37399
|
-
|
|
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
|
+
};
|
|
37400
37655
|
}
|
|
37401
|
-
const
|
|
37402
|
-
|
|
37403
|
-
|
|
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
|
|
37404
37683
|
};
|
|
37405
|
-
await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
37406
|
-
const tempPath = `${path}.${process.pid}.tmp`;
|
|
37407
|
-
await (0, import_promises4.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
|
|
37408
|
-
`, { encoding: "utf8", mode: 384 });
|
|
37409
|
-
await (0, import_promises4.rename)(tempPath, path);
|
|
37410
37684
|
}
|
|
37411
|
-
|
|
37412
|
-
const
|
|
37413
|
-
|
|
37414
|
-
|
|
37415
|
-
|
|
37416
|
-
|
|
37417
|
-
|
|
37418
|
-
|
|
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(", ")}`);
|
|
37419
37706
|
}
|
|
37420
|
-
|
|
37421
|
-
|
|
37422
|
-
const now = Date.now();
|
|
37423
|
-
if (!options.force && state.lastCheckedAt > 0 && now - state.lastCheckedAt < AUTO_SHARE_FETCH_INTERVAL_MS) {
|
|
37424
|
-
return [];
|
|
37707
|
+
if (manifest.deps.cli.length > 0) {
|
|
37708
|
+
lines.push(`- CLI: ${manifest.deps.cli.join(", ")}`);
|
|
37425
37709
|
}
|
|
37426
|
-
|
|
37427
|
-
|
|
37428
|
-
const nextBehindTeams = new Set(state.behindTeams);
|
|
37429
|
-
for (const status of statuses) {
|
|
37430
|
-
if (status.warning) {
|
|
37431
|
-
continue;
|
|
37432
|
-
}
|
|
37433
|
-
if (status.behind > 0) {
|
|
37434
|
-
nextBehindTeams.add(status.team);
|
|
37435
|
-
} else {
|
|
37436
|
-
nextBehindTeams.delete(status.team);
|
|
37437
|
-
}
|
|
37438
|
-
}
|
|
37439
|
-
state.behindTeams = nextBehindTeams;
|
|
37440
|
-
return statuses.flatMap((status) => status.warning ? [status.warning] : []);
|
|
37441
|
-
} finally {
|
|
37442
|
-
state.lastCheckedAt = Date.now();
|
|
37710
|
+
if (manifest.deps.os.length > 0) {
|
|
37711
|
+
lines.push(`- OS: ${manifest.deps.os.join(", ")}`);
|
|
37443
37712
|
}
|
|
37713
|
+
if (manifest.deps.mcp.length > 0) {
|
|
37714
|
+
lines.push(`- MCP (configure separately): ${manifest.deps.mcp.join(", ")}`);
|
|
37715
|
+
}
|
|
37716
|
+
lines.push("", `Install: threadnote share install-artifacts --kind pack --name ${artifact.name} --apply`, "");
|
|
37717
|
+
return lines.join("\n");
|
|
37444
37718
|
}
|
|
37445
|
-
function
|
|
37446
|
-
const
|
|
37447
|
-
|
|
37448
|
-
|
|
37449
|
-
|
|
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
|
+
`;
|
|
37450
37728
|
}
|
|
37451
|
-
|
|
37452
|
-
const
|
|
37453
|
-
|
|
37454
|
-
if (teams.length === 0) {
|
|
37455
|
-
return [];
|
|
37456
|
-
}
|
|
37457
|
-
const git = await requiredExecutable("git");
|
|
37458
|
-
const statuses = [];
|
|
37459
|
-
for (const [name, team] of teams) {
|
|
37460
|
-
const fetchResult = await runCommand(git, ["-C", team.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME], {
|
|
37461
|
-
allowFailure: true
|
|
37462
|
-
});
|
|
37463
|
-
if (fetchResult.exitCode !== 0) {
|
|
37464
|
-
statuses.push({
|
|
37465
|
-
behind: 0,
|
|
37466
|
-
team: name,
|
|
37467
|
-
warning: `Auto-sync check for shared team "${name}" failed: ${fetchResult.stderr.trim() || fetchResult.stdout.trim() || "unknown git fetch error"}`
|
|
37468
|
-
});
|
|
37469
|
-
continue;
|
|
37470
|
-
}
|
|
37471
|
-
const behind = await gitOutput(team.worktree, ["rev-list", "--count", "HEAD..@{u}"], false);
|
|
37472
|
-
if (behind === void 0) {
|
|
37473
|
-
statuses.push({
|
|
37474
|
-
behind: 0,
|
|
37475
|
-
team: name,
|
|
37476
|
-
warning: `Auto-sync check for shared team "${name}" failed: could not read upstream behind count.`
|
|
37477
|
-
});
|
|
37478
|
-
continue;
|
|
37479
|
-
}
|
|
37480
|
-
statuses.push({ behind: Number.parseInt(behind, 10) || 0, team: name });
|
|
37481
|
-
}
|
|
37482
|
-
return statuses;
|
|
37483
|
-
}
|
|
37484
|
-
async function runShareSyncQuiet(config2, state, options) {
|
|
37485
|
-
const team = await resolveTeam(config2, options.team);
|
|
37486
|
-
const git = await requiredExecutable("git");
|
|
37487
|
-
const worktree = team.config.worktree;
|
|
37488
|
-
const pendingChanges = state.pendingReindexes.get(team.name);
|
|
37489
|
-
if (pendingChanges && pendingChanges.length > 0) {
|
|
37490
|
-
await applyAndPersistChanges(config2, team.config, state, pendingChanges, { quiet: true });
|
|
37491
|
-
}
|
|
37492
|
-
if (await hasUncommittedChanges(worktree)) {
|
|
37493
|
-
return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
|
|
37494
|
-
}
|
|
37495
|
-
const ahead = await gitOutput(worktree, ["rev-list", "--count", "@{u}..HEAD"], false);
|
|
37496
|
-
if (ahead === void 0) {
|
|
37497
|
-
return `Shared team "${team.name}" upstream status is unknown; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to inspect and resolve it.`;
|
|
37498
|
-
}
|
|
37499
|
-
if ((Number.parseInt(ahead, 10) || 0) > 0) {
|
|
37500
|
-
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.`;
|
|
37501
|
-
}
|
|
37502
|
-
const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
|
|
37503
|
-
const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
|
|
37504
|
-
if (pullResult.exitCode !== 0) {
|
|
37505
|
-
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"))) {
|
|
37506
|
-
throw new Error(
|
|
37507
|
-
`Automatic share sync hit git conflicts in ${worktree}. Resolve them in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then rerun recall/read.`
|
|
37508
|
-
);
|
|
37509
|
-
}
|
|
37510
|
-
throw new Error(
|
|
37511
|
-
`Automatic share sync failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
|
|
37512
|
-
);
|
|
37513
|
-
}
|
|
37514
|
-
const afterRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
|
|
37515
|
-
if (beforeRev && afterRev && beforeRev !== afterRev) {
|
|
37516
|
-
const changes = await listChangedFiles(worktree, beforeRev, afterRev);
|
|
37517
|
-
if (changes.length > 0) {
|
|
37518
|
-
const stillPending = state.pendingReindexes.get(team.name) ?? [];
|
|
37519
|
-
const combined = mergeChanges(stillPending, changes);
|
|
37520
|
-
await applyAndPersistChanges(config2, team.config, state, combined, { quiet: true });
|
|
37521
|
-
}
|
|
37522
|
-
}
|
|
37523
|
-
return void 0;
|
|
37524
|
-
}
|
|
37525
|
-
async function publishShareGitChange(worktree, relativePath, commitMessage, options = {}) {
|
|
37526
|
-
const dryRun = options.dryRun === true;
|
|
37527
|
-
const push = options.push !== false;
|
|
37528
|
-
const verb = options.verb ?? "add";
|
|
37529
|
-
const git = await requiredExecutable("git");
|
|
37530
|
-
const messages = [];
|
|
37531
|
-
const stageArgs = verb === "rm" ? ["-C", worktree, "rm", relativePath] : ["-C", worktree, "add", "--", relativePath];
|
|
37532
|
-
const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
|
|
37533
|
-
if (stageResult) {
|
|
37534
|
-
messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
|
|
37535
|
-
}
|
|
37536
|
-
if (dryRun) {
|
|
37537
|
-
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "commit", "-m", commitMessage])}`);
|
|
37538
|
-
} else {
|
|
37539
|
-
const commitResult = await runCommand(git, ["-C", worktree, "commit", "-m", commitMessage], { allowFailure: true });
|
|
37540
|
-
if (commitResult.exitCode !== 0) {
|
|
37541
|
-
const detail = commitResult.stdout.trim() || commitResult.stderr.trim();
|
|
37542
|
-
if (/nothing to commit|no changes added/i.test(detail)) {
|
|
37543
|
-
messages.push("git commit: nothing to commit (file already in tree)");
|
|
37544
|
-
} else {
|
|
37545
|
-
throw new Error(`git commit failed: ${detail || "unknown error"}`);
|
|
37546
|
-
}
|
|
37547
|
-
} else {
|
|
37548
|
-
messages.push(`git commit: ${commitResult.stdout.trim().split("\n").slice(0, 2).join(" ")}`);
|
|
37549
|
-
}
|
|
37550
|
-
}
|
|
37551
|
-
if (!push) {
|
|
37552
|
-
messages.push("git push skipped (push=false)");
|
|
37553
|
-
return messages;
|
|
37554
|
-
}
|
|
37555
|
-
const pushResult = await runGitCommand(
|
|
37556
|
-
dryRun,
|
|
37557
|
-
git,
|
|
37558
|
-
["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME],
|
|
37559
|
-
"git push failed"
|
|
37560
|
-
);
|
|
37561
|
-
if (pushResult) {
|
|
37562
|
-
messages.push(`git push: ${pushResult.stdout.trim() || pushResult.stderr.trim() || "ok"}`);
|
|
37563
|
-
}
|
|
37564
|
-
return messages;
|
|
37565
|
-
}
|
|
37566
|
-
async function runGitCommand(dryRun, git, args, failureLabel) {
|
|
37567
|
-
if (dryRun) {
|
|
37568
|
-
console.log(`Would run: ${formatShellCommand(git, args)}`);
|
|
37569
|
-
return void 0;
|
|
37570
|
-
}
|
|
37571
|
-
const result = await runCommand(git, args, { allowFailure: true });
|
|
37572
|
-
if (result.exitCode !== 0) {
|
|
37573
|
-
throw new Error(`${failureLabel}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`);
|
|
37574
|
-
}
|
|
37575
|
-
return result;
|
|
37729
|
+
function packSkillName(skillEntry) {
|
|
37730
|
+
const trimmed = skillEntry.replace(/\/SKILL\.md$/i, "");
|
|
37731
|
+
return (0, import_node_path3.basename)(trimmed);
|
|
37576
37732
|
}
|
|
37577
|
-
async function
|
|
37733
|
+
async function shareBundlePack(config2, manifestPath, options) {
|
|
37578
37734
|
const team = await resolveTeam(config2, options.team);
|
|
37579
37735
|
const dryRun = options.dryRun === true;
|
|
37580
37736
|
const preview = options.preview === true;
|
|
37581
|
-
const
|
|
37582
|
-
if (!await isRegularFileNoSymlink(
|
|
37583
|
-
throw new Error(`
|
|
37584
|
-
}
|
|
37585
|
-
const
|
|
37586
|
-
const
|
|
37587
|
-
|
|
37588
|
-
|
|
37589
|
-
|
|
37590
|
-
const
|
|
37591
|
-
const
|
|
37592
|
-
const
|
|
37593
|
-
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 });
|
|
37594
37761
|
const messages = [
|
|
37595
|
-
`${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"}
|
|
37596
|
-
`Source: ${portablePath(
|
|
37597
|
-
`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}`
|
|
37598
37765
|
];
|
|
37766
|
+
const blockers = prepared.filter((entry) => entry.blocker !== void 0);
|
|
37599
37767
|
if (preview) {
|
|
37600
|
-
|
|
37601
|
-
|
|
37602
|
-
|
|
37603
|
-
|
|
37604
|
-
|
|
37605
|
-
|
|
37606
|
-
|
|
37607
|
-
targetPath,
|
|
37608
|
-
targetUri
|
|
37609
|
-
};
|
|
37610
|
-
}
|
|
37611
|
-
for (const redaction of scrub.redactions) {
|
|
37612
|
-
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}`);
|
|
37613
37775
|
}
|
|
37614
37776
|
return {
|
|
37615
37777
|
artifact,
|
|
37616
37778
|
gitMessages: [],
|
|
37617
37779
|
messages,
|
|
37618
|
-
previewContent:
|
|
37619
|
-
sourcePath:
|
|
37620
|
-
targetPath,
|
|
37621
|
-
targetUri
|
|
37780
|
+
previewContent: indexScrub.cleaned,
|
|
37781
|
+
sourcePath: resolvedManifest,
|
|
37782
|
+
targetPath: indexTargetPath,
|
|
37783
|
+
targetUri: indexTargetUri
|
|
37622
37784
|
};
|
|
37623
37785
|
}
|
|
37624
|
-
|
|
37786
|
+
const indexResidual = residualRewriteRoot(indexScrub.cleaned, rewriteRoots);
|
|
37787
|
+
if (indexScrub.blocker !== void 0 || indexResidual !== void 0) {
|
|
37625
37788
|
throw new Error(
|
|
37626
|
-
`Refusing to share ${
|
|
37789
|
+
`Refusing to share pack ${artifact.agent}/${artifact.name}: index ${indexScrub.blocker ?? `machine-local path "${indexResidual}" not rewritten`}.`
|
|
37627
37790
|
);
|
|
37628
37791
|
}
|
|
37629
|
-
|
|
37630
|
-
|
|
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
|
+
);
|
|
37631
37796
|
}
|
|
37632
|
-
const
|
|
37633
|
-
|
|
37634
|
-
|
|
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) {
|
|
37635
37802
|
throw new Error(
|
|
37636
|
-
`
|
|
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("; ")}`
|
|
37637
37828
|
);
|
|
37638
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
|
+
}
|
|
37639
37838
|
if (dryRun) {
|
|
37640
|
-
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
|
+
};
|
|
37641
37848
|
}
|
|
37642
37849
|
const ov = await openVikingCliForMode(dryRun);
|
|
37643
|
-
const
|
|
37644
|
-
|
|
37645
|
-
|
|
37646
|
-
|
|
37647
|
-
|
|
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, {
|
|
37648
37932
|
dryRun,
|
|
37649
37933
|
push: options.push
|
|
37650
37934
|
});
|
|
37651
|
-
return {
|
|
37935
|
+
return {
|
|
37936
|
+
artifact,
|
|
37937
|
+
gitMessages,
|
|
37938
|
+
messages,
|
|
37939
|
+
sourcePath: resolvedManifest,
|
|
37940
|
+
targetPath: indexTargetPath,
|
|
37941
|
+
targetUri: indexTargetUri
|
|
37942
|
+
};
|
|
37652
37943
|
}
|
|
37653
37944
|
async function listSharedAgentArtifacts(config2, options = {}) {
|
|
37654
37945
|
const syncResult = await maybeSyncSharedArtifacts(config2, options);
|
|
@@ -37691,6 +37982,10 @@ async function installSharedAgentArtifacts(config2, options) {
|
|
|
37691
37982
|
}
|
|
37692
37983
|
let installedCount = 0;
|
|
37693
37984
|
for (const artifact of artifacts) {
|
|
37985
|
+
if (isBundleArtifact(artifact)) {
|
|
37986
|
+
installedCount += await installBundleArtifact(artifact, options, dryRun, messages);
|
|
37987
|
+
continue;
|
|
37988
|
+
}
|
|
37694
37989
|
const label = sharedArtifactLabel(artifact.artifact);
|
|
37695
37990
|
const state = await sharedArtifactInstallState(artifact);
|
|
37696
37991
|
if (dryRun) {
|
|
@@ -37707,7 +38002,7 @@ async function installSharedAgentArtifacts(config2, options) {
|
|
|
37707
38002
|
messages.push(`Already installed ${label}: ${portablePath(artifact.installPath)}`);
|
|
37708
38003
|
continue;
|
|
37709
38004
|
}
|
|
37710
|
-
await ensureDirectory((0,
|
|
38005
|
+
await ensureDirectory((0, import_node_path3.dirname)(artifact.installPath), false);
|
|
37711
38006
|
await (0, import_promises4.writeFile)(artifact.installPath, state.sourceContent, { encoding: "utf8", mode: 384 });
|
|
37712
38007
|
await writeSharedArtifactMetadata(artifact, state.sourceSha);
|
|
37713
38008
|
installedCount += 1;
|
|
@@ -37736,10 +38031,10 @@ function normalizeTeamName(input) {
|
|
|
37736
38031
|
return candidate;
|
|
37737
38032
|
}
|
|
37738
38033
|
function teamsFilePath(config2) {
|
|
37739
|
-
return (0,
|
|
38034
|
+
return (0, import_node_path3.join)(config2.agentContextHome, "share", "teams.json");
|
|
37740
38035
|
}
|
|
37741
38036
|
function teamWorktreePath(config2, team) {
|
|
37742
|
-
return (0,
|
|
38037
|
+
return (0, import_node_path3.join)(
|
|
37743
38038
|
config2.agentContextHome,
|
|
37744
38039
|
"data",
|
|
37745
38040
|
"viking",
|
|
@@ -37752,7 +38047,7 @@ function teamWorktreePath(config2, team) {
|
|
|
37752
38047
|
);
|
|
37753
38048
|
}
|
|
37754
38049
|
function teamGitdirPath(config2, team) {
|
|
37755
|
-
return (0,
|
|
38050
|
+
return (0, import_node_path3.join)(config2.agentContextHome, "share", "teams", `${team}.gitdir`);
|
|
37756
38051
|
}
|
|
37757
38052
|
async function readTeamsFile(config2) {
|
|
37758
38053
|
const path = teamsFilePath(config2);
|
|
@@ -37808,7 +38103,7 @@ async function resolveTeam(config2, requested) {
|
|
|
37808
38103
|
return { config: found, name: wantName };
|
|
37809
38104
|
}
|
|
37810
38105
|
function workfileToVikingUri(config2, team, filePath) {
|
|
37811
|
-
const rel = (0,
|
|
38106
|
+
const rel = (0, import_node_path3.relative)(team.worktree, filePath).split(import_node_path3.sep).join("/");
|
|
37812
38107
|
return `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
37813
38108
|
}
|
|
37814
38109
|
function isInSharedNamespace(config2, uri) {
|
|
@@ -37866,15 +38161,15 @@ async function isRegularFileNoSymlink(path) {
|
|
|
37866
38161
|
}
|
|
37867
38162
|
}
|
|
37868
38163
|
function inferShareArtifact(path, options) {
|
|
37869
|
-
const normalizedPath = path.split(
|
|
37870
|
-
const fileName = (0,
|
|
38164
|
+
const normalizedPath = path.split(import_node_path3.sep).join("/");
|
|
38165
|
+
const fileName = (0, import_node_path3.basename)(path);
|
|
37871
38166
|
const lowerFileName = fileName.toLowerCase();
|
|
37872
38167
|
const lowerPath = normalizedPath.toLowerCase();
|
|
37873
38168
|
const inferredKind = lowerFileName === "skill.md" ? "skill" : lowerPath.includes("/.claude/commands/") && lowerFileName.endsWith(".md") ? "command" : void 0;
|
|
37874
38169
|
const inferredAgent = lowerPath.includes("/.codex/skills/") ? "codex" : lowerPath.includes("/.claude/skills/") || lowerPath.includes("/.claude/commands/") ? "claude" : void 0;
|
|
37875
38170
|
const extensionIndex = fileName.lastIndexOf(".");
|
|
37876
38171
|
const stem = extensionIndex > 0 ? fileName.slice(0, extensionIndex) : fileName;
|
|
37877
|
-
const inferredName = lowerFileName === "skill.md" ? (0,
|
|
38172
|
+
const inferredName = lowerFileName === "skill.md" ? (0, import_node_path3.basename)((0, import_node_path3.dirname)(path)) : stem;
|
|
37878
38173
|
const kind = options.kind ?? inferredKind;
|
|
37879
38174
|
const agent = options.agent ?? inferredAgent;
|
|
37880
38175
|
const name = options.name ?? inferredName;
|
|
@@ -37915,10 +38210,13 @@ function sharedArtifactFromRelativePath(relativePath) {
|
|
|
37915
38210
|
if (parts.length === 4 && parts[1] === "commands" && parts[2] === "claude" && parts[3].endsWith(".md")) {
|
|
37916
38211
|
return { agent: "claude", kind: "command", name: parts[3].slice(0, -".md".length) };
|
|
37917
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
|
+
}
|
|
37918
38216
|
return void 0;
|
|
37919
38217
|
}
|
|
37920
38218
|
async function collectSharedArtifacts(worktree, team) {
|
|
37921
|
-
const root = (0,
|
|
38219
|
+
const root = (0, import_node_path3.join)(worktree, SHAREABLE_ARTIFACT_DIR);
|
|
37922
38220
|
if (!await isDirectory(root)) {
|
|
37923
38221
|
return [];
|
|
37924
38222
|
}
|
|
@@ -37926,7 +38224,7 @@ async function collectSharedArtifacts(worktree, team) {
|
|
|
37926
38224
|
async function visit(path) {
|
|
37927
38225
|
const entries = await (0, import_promises4.readdir)(path, { withFileTypes: true });
|
|
37928
38226
|
for (const entry of entries) {
|
|
37929
|
-
const full = (0,
|
|
38227
|
+
const full = (0, import_node_path3.join)(path, entry.name);
|
|
37930
38228
|
if (entry.isDirectory()) {
|
|
37931
38229
|
await visit(full);
|
|
37932
38230
|
continue;
|
|
@@ -37934,23 +38232,100 @@ async function collectSharedArtifacts(worktree, team) {
|
|
|
37934
38232
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
37935
38233
|
continue;
|
|
37936
38234
|
}
|
|
37937
|
-
const relativePath = (0,
|
|
38235
|
+
const relativePath = (0, import_node_path3.relative)(worktree, full).split(import_node_path3.sep).join("/");
|
|
37938
38236
|
const artifact = sharedArtifactFromRelativePath(relativePath);
|
|
37939
38237
|
if (artifact === void 0) {
|
|
37940
38238
|
continue;
|
|
37941
38239
|
}
|
|
37942
|
-
|
|
37943
|
-
|
|
37944
|
-
|
|
37945
|
-
|
|
37946
|
-
|
|
37947
|
-
|
|
37948
|
-
}
|
|
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
|
+
}
|
|
37949
38259
|
}
|
|
37950
38260
|
}
|
|
37951
38261
|
await visit(root);
|
|
37952
38262
|
return out.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
|
|
37953
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
|
+
}
|
|
37954
38329
|
function filterSharedArtifacts(artifacts, options) {
|
|
37955
38330
|
const name = options.name === void 0 ? void 0 : uriSegment(options.name);
|
|
37956
38331
|
return artifacts.filter((artifact) => {
|
|
@@ -37973,13 +38348,228 @@ async function maybeSyncSharedArtifacts(config2, options) {
|
|
|
37973
38348
|
return syncSharedReposBeforeAgentRead(config2);
|
|
37974
38349
|
}
|
|
37975
38350
|
async function sharedArtifactInstallStatus(artifact) {
|
|
38351
|
+
if (isBundleArtifact(artifact)) {
|
|
38352
|
+
return sharedBundleInstallStatus(artifact);
|
|
38353
|
+
}
|
|
37976
38354
|
return (await sharedArtifactInstallState(artifact)).status;
|
|
37977
38355
|
}
|
|
37978
|
-
|
|
37979
|
-
|
|
37980
|
-
|
|
37981
|
-
|
|
37982
|
-
|
|
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
|
+
}
|
|
38568
|
+
async function sharedArtifactInstallState(artifact) {
|
|
38569
|
+
const sourceContent = await (0, import_promises4.readFile)(artifact.sourcePath, "utf8");
|
|
38570
|
+
const sourceSha = sha256(sourceContent);
|
|
38571
|
+
const existingContent = await readFileIfExists(artifact.installPath) ?? void 0;
|
|
38572
|
+
if (existingContent === void 0) {
|
|
37983
38573
|
return { sourceContent, sourceSha, status: "not_installed" };
|
|
37984
38574
|
}
|
|
37985
38575
|
const existingSha = sha256(existingContent);
|
|
@@ -38045,7 +38635,7 @@ async function writeSharedArtifactMetadata(artifact, sourceSha) {
|
|
|
38045
38635
|
version: ARTIFACT_INSTALL_METADATA_VERSION
|
|
38046
38636
|
};
|
|
38047
38637
|
const metadataPath = sharedArtifactMetadataPath(artifact);
|
|
38048
|
-
await ensureDirectory((0,
|
|
38638
|
+
await ensureDirectory((0, import_node_path3.dirname)(metadataPath), false);
|
|
38049
38639
|
await (0, import_promises4.writeFile)(metadataPath, `${JSON.stringify(metadata, void 0, 2)}
|
|
38050
38640
|
`, { encoding: "utf8", mode: 384 });
|
|
38051
38641
|
}
|
|
@@ -38097,336 +38687,834 @@ function sharedArtifactLabel(artifact) {
|
|
|
38097
38687
|
return `${artifact.kind} ${artifact.agent}/${artifact.name}`;
|
|
38098
38688
|
}
|
|
38099
38689
|
function sharedArtifactInstallPath(team, artifact) {
|
|
38100
|
-
|
|
38101
|
-
|
|
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]);
|
|
38714
|
+
}
|
|
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
|
+
);
|
|
38102
38856
|
}
|
|
38103
|
-
|
|
38104
|
-
|
|
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.");
|
|
38105
39276
|
}
|
|
38106
|
-
return
|
|
39277
|
+
return lines.join("\n");
|
|
38107
39278
|
}
|
|
38108
|
-
function
|
|
38109
|
-
const
|
|
38110
|
-
|
|
38111
|
-
|
|
38112
|
-
|
|
38113
|
-
|
|
38114
|
-
|
|
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;
|
|
38115
39288
|
}
|
|
39289
|
+
groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
|
|
38116
39290
|
}
|
|
38117
|
-
const
|
|
38118
|
-
|
|
38119
|
-
if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
|
|
39291
|
+
for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
|
|
39292
|
+
if (group.length < 3) {
|
|
38120
39293
|
continue;
|
|
38121
39294
|
}
|
|
38122
|
-
|
|
38123
|
-
|
|
38124
|
-
for (let index = headerEnd; index < lines.length; index += 1) {
|
|
38125
|
-
cleaned.push(lines[index]);
|
|
38126
|
-
}
|
|
38127
|
-
return cleaned.join("\n");
|
|
38128
|
-
}
|
|
38129
|
-
async function ensureSharedDirectoryChain(config2, ov, memoryUri, dryRun, options = {}) {
|
|
38130
|
-
const directoryUri = parentUri(memoryUri);
|
|
38131
|
-
for (const uri of sharedDirectoryChain(config2, directoryUri)) {
|
|
38132
|
-
const args = withIdentity(config2, ["stat", uri]);
|
|
38133
|
-
if (dryRun) {
|
|
38134
|
-
if (options.quiet !== true) {
|
|
38135
|
-
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
38136
|
-
}
|
|
39295
|
+
const first = group[0];
|
|
39296
|
+
if (!first || !isCompactableKind(first.record.metadata.kind)) {
|
|
38137
39297
|
continue;
|
|
38138
39298
|
}
|
|
38139
|
-
const
|
|
38140
|
-
if (
|
|
39299
|
+
const topic = first.topic;
|
|
39300
|
+
if (!topic) {
|
|
38141
39301
|
continue;
|
|
38142
39302
|
}
|
|
38143
|
-
|
|
38144
|
-
|
|
38145
|
-
|
|
38146
|
-
|
|
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;
|
|
38147
39318
|
}
|
|
39319
|
+
nudges.push(
|
|
39320
|
+
`Many active handoffs surfaced for ${project}; run compact_context({"project":"${project}","dryRun":true}).`
|
|
39321
|
+
);
|
|
38148
39322
|
}
|
|
39323
|
+
return [...new Set(nudges)];
|
|
38149
39324
|
}
|
|
38150
|
-
function
|
|
38151
|
-
const
|
|
38152
|
-
|
|
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)];
|
|
38153
39337
|
}
|
|
38154
|
-
function
|
|
38155
|
-
const prefix = `viking://user/${uriSegment(
|
|
38156
|
-
if (!
|
|
38157
|
-
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;
|
|
38158
39342
|
}
|
|
38159
|
-
const
|
|
38160
|
-
const
|
|
38161
|
-
|
|
38162
|
-
|
|
39343
|
+
const rest = uri.slice(prefix.length);
|
|
39344
|
+
const parts = rest.split("/").filter(Boolean);
|
|
39345
|
+
if (parts.length < 4) {
|
|
39346
|
+
return void 0;
|
|
38163
39347
|
}
|
|
38164
|
-
|
|
38165
|
-
}
|
|
38166
|
-
async function writeMemoryFile(config2, ov, uri, content, initialMode, dryRun, options = {}) {
|
|
38167
|
-
if (dryRun) {
|
|
38168
|
-
const args = withIdentity(config2, [
|
|
38169
|
-
"write",
|
|
38170
|
-
uri,
|
|
38171
|
-
"--from-file",
|
|
38172
|
-
"<staged temp file>",
|
|
38173
|
-
"--mode",
|
|
38174
|
-
initialMode,
|
|
38175
|
-
"--wait",
|
|
38176
|
-
"--timeout",
|
|
38177
|
-
"120"
|
|
38178
|
-
]);
|
|
38179
|
-
if (options.quiet !== true) {
|
|
38180
|
-
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
38181
|
-
}
|
|
38182
|
-
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$/, "") };
|
|
38183
39350
|
}
|
|
38184
|
-
|
|
38185
|
-
|
|
38186
|
-
|
|
38187
|
-
|
|
38188
|
-
|
|
38189
|
-
await refreshMemoryIndex(config2, ov, uri, options);
|
|
38190
|
-
} finally {
|
|
38191
|
-
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$/, "") };
|
|
38192
39356
|
}
|
|
39357
|
+
return void 0;
|
|
38193
39358
|
}
|
|
38194
|
-
|
|
38195
|
-
|
|
38196
|
-
|
|
38197
|
-
|
|
38198
|
-
|
|
38199
|
-
|
|
38200
|
-
|
|
38201
|
-
const mode = attempt === 0 ? initialMode : ourWriteLanded ? "replace" : initialMode;
|
|
38202
|
-
const args = withIdentity(config2, [
|
|
38203
|
-
"write",
|
|
38204
|
-
uri,
|
|
38205
|
-
"--from-file",
|
|
38206
|
-
fromFile,
|
|
38207
|
-
"--mode",
|
|
38208
|
-
mode,
|
|
38209
|
-
"--wait",
|
|
38210
|
-
"--timeout",
|
|
38211
|
-
"120"
|
|
38212
|
-
]);
|
|
38213
|
-
if (options.quiet !== true) {
|
|
38214
|
-
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
38215
|
-
}
|
|
38216
|
-
const result = await runCommand(ov, args, { allowFailure: true });
|
|
38217
|
-
if (result.exitCode === 0) {
|
|
38218
|
-
if (options.quiet !== true && result.stdout.trim()) {
|
|
38219
|
-
console.log(result.stdout.trim());
|
|
38220
|
-
}
|
|
38221
|
-
if (options.quiet !== true && result.stderr.trim()) {
|
|
38222
|
-
console.error(result.stderr.trim());
|
|
38223
|
-
}
|
|
38224
|
-
return;
|
|
38225
|
-
}
|
|
38226
|
-
if (isTransientOvFailure(result.stderr, result.stdout) && await vikingResourceExists(ov, config2, uri) && !existedBeforeWrite) {
|
|
38227
|
-
if (options.quiet !== true) {
|
|
38228
|
-
console.log(
|
|
38229
|
-
"OpenViking accepted the write but returned an error before the wait completed; draining the queue."
|
|
38230
|
-
);
|
|
38231
|
-
}
|
|
38232
|
-
await waitForOvQueue(ov, config2, options);
|
|
38233
|
-
return;
|
|
38234
|
-
}
|
|
38235
|
-
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
38236
|
-
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
38237
|
-
}
|
|
38238
|
-
if (isResourceBusyFailure(result.stderr, result.stdout)) {
|
|
38239
|
-
await waitForOvQueue(ov, config2, options);
|
|
38240
|
-
await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
|
|
38241
|
-
} else {
|
|
38242
|
-
await sleep(1e3 * (attempt + 1));
|
|
38243
|
-
}
|
|
39359
|
+
function groupableRecord(record2) {
|
|
39360
|
+
if (record2.metadata.status !== "active" || !isCompactableKind(record2.metadata.kind)) {
|
|
39361
|
+
return void 0;
|
|
39362
|
+
}
|
|
39363
|
+
const project = normalizeOptionalMetadata(record2.metadata.project) ?? parseProjectFromUri(record2.uri);
|
|
39364
|
+
if (!project) {
|
|
39365
|
+
return void 0;
|
|
38244
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 };
|
|
38245
39370
|
}
|
|
38246
|
-
|
|
38247
|
-
|
|
38248
|
-
|
|
38249
|
-
|
|
38250
|
-
|
|
38251
|
-
);
|
|
38252
|
-
|
|
38253
|
-
|
|
38254
|
-
|
|
38255
|
-
|
|
38256
|
-
|
|
38257
|
-
|
|
38258
|
-
|
|
38259
|
-
|
|
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];
|
|
38260
39386
|
}
|
|
38261
|
-
if (
|
|
38262
|
-
|
|
38263
|
-
|
|
38264
|
-
|
|
39387
|
+
if (parts[0] === "durable" && parts[1] === "projects") {
|
|
39388
|
+
return parts[2];
|
|
39389
|
+
}
|
|
39390
|
+
return void 0;
|
|
39391
|
+
}
|
|
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();
|
|
38265
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;
|
|
38266
39420
|
}
|
|
38267
|
-
|
|
38268
|
-
|
|
38269
|
-
|
|
38270
|
-
console.log(result.stdout.trim());
|
|
39421
|
+
function isStaleLookingHandoff(record2, now) {
|
|
39422
|
+
if (record2.metadata.kind !== "handoff") {
|
|
39423
|
+
return false;
|
|
38271
39424
|
}
|
|
38272
|
-
if (
|
|
38273
|
-
|
|
39425
|
+
if (now.getTime() - timestampMs(record2) < STALE_HANDOFF_AGE_MS) {
|
|
39426
|
+
return false;
|
|
38274
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
|
+
);
|
|
38275
39431
|
}
|
|
38276
|
-
function
|
|
38277
|
-
const
|
|
38278
|
-
|
|
38279
|
-
|
|
38280
|
-
|
|
38281
|
-
|
|
38282
|
-
|
|
38283
|
-
${stdout2}`.toLowerCase();
|
|
38284
|
-
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;
|
|
38285
39439
|
}
|
|
38286
|
-
|
|
38287
|
-
|
|
38288
|
-
await writeMemoryFile(config2, ov, uri, content, initialMode, false, options);
|
|
39440
|
+
function compareGroupedRecordLists(left, right) {
|
|
39441
|
+
return (left[0]?.groupKey ?? "").localeCompare(right[0]?.groupKey ?? "");
|
|
38289
39442
|
}
|
|
38290
|
-
|
|
38291
|
-
const
|
|
38292
|
-
|
|
38293
|
-
|
|
38294
|
-
|
|
38295
|
-
|
|
38296
|
-
return;
|
|
38297
|
-
}
|
|
38298
|
-
const maxAttempts = BUSY_RETRY_BACKOFF_MS.length + 1;
|
|
38299
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
38300
|
-
const result = await runCommand(ov, args, { allowFailure: true });
|
|
38301
|
-
if (result.exitCode === 0) {
|
|
38302
|
-
if (options.quiet !== true && result.stdout.trim()) {
|
|
38303
|
-
console.log(result.stdout.trim());
|
|
38304
|
-
}
|
|
38305
|
-
return;
|
|
38306
|
-
}
|
|
38307
|
-
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
38308
|
-
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
38309
|
-
}
|
|
38310
|
-
if (isResourceBusyFailure(result.stderr, result.stdout)) {
|
|
38311
|
-
await waitForOvQueue(ov, config2, options);
|
|
38312
|
-
await sleep(BUSY_RETRY_BACKOFF_MS[attempt] ?? 3e4);
|
|
38313
|
-
} else {
|
|
38314
|
-
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;
|
|
38315
39449
|
}
|
|
39450
|
+
seen.add(item.uri);
|
|
39451
|
+
result.push(item);
|
|
38316
39452
|
}
|
|
39453
|
+
return result;
|
|
38317
39454
|
}
|
|
38318
|
-
|
|
38319
|
-
const
|
|
38320
|
-
|
|
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");
|
|
38321
39468
|
}
|
|
38322
|
-
|
|
38323
|
-
const
|
|
38324
|
-
return
|
|
39469
|
+
function headerValue(header, key) {
|
|
39470
|
+
const prefix = `${key}:`;
|
|
39471
|
+
return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
|
|
38325
39472
|
}
|
|
38326
|
-
|
|
38327
|
-
if (
|
|
39473
|
+
function parseOptionalMemoryKind(value) {
|
|
39474
|
+
if (!value) {
|
|
38328
39475
|
return void 0;
|
|
38329
39476
|
}
|
|
38330
|
-
|
|
38331
|
-
|
|
38332
|
-
return void 0;
|
|
39477
|
+
if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
|
|
39478
|
+
return value;
|
|
38333
39479
|
}
|
|
38334
|
-
return
|
|
39480
|
+
return void 0;
|
|
38335
39481
|
}
|
|
38336
|
-
|
|
38337
|
-
|
|
38338
|
-
|
|
38339
|
-
});
|
|
38340
|
-
if (result.exitCode !== 0) {
|
|
38341
|
-
return [];
|
|
39482
|
+
function parseOptionalMemoryStatus(value) {
|
|
39483
|
+
if (!value) {
|
|
39484
|
+
return void 0;
|
|
38342
39485
|
}
|
|
38343
|
-
|
|
38344
|
-
|
|
38345
|
-
for (let index = 0; index < entries.length; ) {
|
|
38346
|
-
const raw = entries[index];
|
|
38347
|
-
const head = raw.slice(0, 1);
|
|
38348
|
-
if (head === "R" || head === "C") {
|
|
38349
|
-
const oldRel = entries[index + 1];
|
|
38350
|
-
const newRel = entries[index + 2];
|
|
38351
|
-
if (oldRel && newRel) {
|
|
38352
|
-
changes.push({ path: (0, import_node_path4.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
38353
|
-
changes.push({ path: (0, import_node_path4.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
38354
|
-
}
|
|
38355
|
-
index += 3;
|
|
38356
|
-
continue;
|
|
38357
|
-
}
|
|
38358
|
-
const rel = entries[index + 1];
|
|
38359
|
-
if (rel) {
|
|
38360
|
-
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
38361
|
-
changes.push({ path: (0, import_node_path4.join)(worktree, rel), relativePath: rel, status });
|
|
38362
|
-
}
|
|
38363
|
-
index += 2;
|
|
39486
|
+
if (["active", "archived", "superseded"].includes(value)) {
|
|
39487
|
+
return value;
|
|
38364
39488
|
}
|
|
38365
|
-
return
|
|
39489
|
+
return void 0;
|
|
38366
39490
|
}
|
|
38367
|
-
|
|
38368
|
-
const
|
|
38369
|
-
|
|
38370
|
-
for (const change of changes) {
|
|
38371
|
-
if (!change.relativePath.endsWith(".md")) {
|
|
38372
|
-
continue;
|
|
38373
|
-
}
|
|
38374
|
-
const firstSegment = change.relativePath.split("/")[0];
|
|
38375
|
-
if (!SHAREABLE_TOP_LEVEL_DIRS.includes(firstSegment)) {
|
|
38376
|
-
continue;
|
|
38377
|
-
}
|
|
38378
|
-
const uri = workfileToVikingUri(config2, team, change.path);
|
|
38379
|
-
try {
|
|
38380
|
-
if (change.status === "removed") {
|
|
38381
|
-
await removeMemoryUri(config2, ov, uri, false, options);
|
|
38382
|
-
continue;
|
|
38383
|
-
}
|
|
38384
|
-
if (!await isFile(change.path)) {
|
|
38385
|
-
continue;
|
|
38386
|
-
}
|
|
38387
|
-
const ovHasResource = await vikingResourceExists(ov, config2, uri);
|
|
38388
|
-
if (ovHasResource) {
|
|
38389
|
-
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)";
|
|
38390
|
-
if (options.quiet !== true) {
|
|
38391
|
-
console.warn(`share sync: ${uri}: ${reason}.`);
|
|
38392
|
-
}
|
|
38393
|
-
}
|
|
38394
|
-
await ensureSharedDirectoryChain(config2, ov, uri, false, options);
|
|
38395
|
-
const writeMode = ovHasResource ? "replace" : "create";
|
|
38396
|
-
await ingestSingleFile(ov, config2, uri, change.path, writeMode, options);
|
|
38397
|
-
} catch (err) {
|
|
38398
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
38399
|
-
if (options.quiet !== true) {
|
|
38400
|
-
console.warn(`share sync: ${uri}: ingest failed \u2014 will retry on the next sync. ${message}`);
|
|
38401
|
-
}
|
|
38402
|
-
failed.push(change);
|
|
38403
|
-
}
|
|
38404
|
-
}
|
|
38405
|
-
return { failed };
|
|
39491
|
+
function normalizeOptionalMetadata(value) {
|
|
39492
|
+
const trimmed = value?.trim();
|
|
39493
|
+
return trimmed ? trimmed : void 0;
|
|
38406
39494
|
}
|
|
38407
|
-
function
|
|
38408
|
-
|
|
38409
|
-
for (const list of lists) {
|
|
38410
|
-
for (const change of list) {
|
|
38411
|
-
map3.set(change.relativePath, change);
|
|
38412
|
-
}
|
|
38413
|
-
}
|
|
38414
|
-
return [...map3.values()];
|
|
39495
|
+
function isCompactableKind(kind) {
|
|
39496
|
+
return kind === "durable" || kind === "handoff" || kind === "incident";
|
|
38415
39497
|
}
|
|
38416
|
-
|
|
38417
|
-
|
|
38418
|
-
|
|
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";
|
|
38419
39510
|
}
|
|
38420
|
-
|
|
38421
|
-
|
|
38422
|
-
|
|
38423
|
-
|
|
38424
|
-
|
|
38425
|
-
} else {
|
|
38426
|
-
state.pendingReindexes.delete(team.name);
|
|
39511
|
+
}
|
|
39512
|
+
function formatPlanSection(title, lines) {
|
|
39513
|
+
if (lines.length === 0) {
|
|
39514
|
+
return `${title}:
|
|
39515
|
+
- none`;
|
|
38427
39516
|
}
|
|
38428
|
-
|
|
38429
|
-
return result;
|
|
39517
|
+
return [`${title}:`, ...lines.map((line) => `- ${line}`)].join("\n");
|
|
38430
39518
|
}
|
|
38431
39519
|
|
|
38432
39520
|
// src/mcp_server.ts
|
|
@@ -38509,6 +39597,15 @@ function registerTools(server, config2) {
|
|
|
38509
39597
|
);
|
|
38510
39598
|
registerArchiveTool(server, config2, "archive", "Compatibility alias for archive_context.");
|
|
38511
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
|
+
);
|
|
38512
39609
|
server.registerTool(
|
|
38513
39610
|
"forget",
|
|
38514
39611
|
{
|
|
@@ -38655,9 +39752,10 @@ function registerTools(server, config2) {
|
|
|
38655
39752
|
"share_skill",
|
|
38656
39753
|
{
|
|
38657
39754
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
38658
|
-
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.",
|
|
38659
39756
|
inputSchema: {
|
|
38660
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"),
|
|
38661
39759
|
force: external_exports.boolean().optional().describe("Replace an existing shared artifact with different content"),
|
|
38662
39760
|
kind: external_exports.enum(["skill", "command"]).optional().describe("Artifact kind when path inference is ambiguous"),
|
|
38663
39761
|
message: external_exports.string().optional().describe('Commit message override; defaults to "share: publish <path>"'),
|
|
@@ -38669,7 +39767,7 @@ function registerTools(server, config2) {
|
|
|
38669
39767
|
team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
|
|
38670
39768
|
}
|
|
38671
39769
|
},
|
|
38672
|
-
async ({ agent, force, kind, message, name, path, preview, push, redact, team }) => {
|
|
39770
|
+
async ({ agent, allowBinary, force, kind, message, name, path, preview, push, redact, team }) => {
|
|
38673
39771
|
const checkedPath = requiredText(path, "share_skill", "path", {
|
|
38674
39772
|
path: "~/.codex/skills/example/SKILL.md"
|
|
38675
39773
|
});
|
|
@@ -38678,6 +39776,7 @@ function registerTools(server, config2) {
|
|
|
38678
39776
|
}
|
|
38679
39777
|
return runShareSkillTool(config2, checkedPath.value, {
|
|
38680
39778
|
agent,
|
|
39779
|
+
allowBinary,
|
|
38681
39780
|
force,
|
|
38682
39781
|
kind,
|
|
38683
39782
|
message,
|
|
@@ -38689,14 +39788,40 @@ function registerTools(server, config2) {
|
|
|
38689
39788
|
});
|
|
38690
39789
|
}
|
|
38691
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
|
+
);
|
|
38692
39817
|
server.registerTool(
|
|
38693
39818
|
"list_shared_skills",
|
|
38694
39819
|
{
|
|
38695
39820
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
38696
|
-
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.",
|
|
38697
39822
|
inputSchema: {
|
|
38698
39823
|
agent: external_exports.enum(["codex", "claude"]).optional().describe("Optional agent filter"),
|
|
38699
|
-
kind: external_exports.enum(["skill", "command"]).optional().describe("Optional kind filter"),
|
|
39824
|
+
kind: external_exports.enum(["skill", "command", "pack"]).optional().describe("Optional kind filter"),
|
|
38700
39825
|
name: external_exports.string().optional().describe("Optional shared artifact name filter"),
|
|
38701
39826
|
team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
|
|
38702
39827
|
}
|
|
@@ -38712,7 +39837,7 @@ function registerTools(server, config2) {
|
|
|
38712
39837
|
agent: external_exports.enum(["codex", "claude"]).optional().describe("Agent owner; required when name is ambiguous"),
|
|
38713
39838
|
dryRun: external_exports.boolean().optional().describe("Preview install without writing local files"),
|
|
38714
39839
|
force: external_exports.boolean().optional().describe("Replace an existing installed artifact with different content"),
|
|
38715
|
-
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"),
|
|
38716
39841
|
name: external_exports.string().optional().describe("Required shared artifact name to install"),
|
|
38717
39842
|
team: external_exports.string().optional().describe("Team name; defaults to the configured default team")
|
|
38718
39843
|
}
|
|
@@ -40098,6 +41223,34 @@ async function runShareSkillTool(config2, sourcePath, options) {
|
|
|
40098
41223
|
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
40099
41224
|
}
|
|
40100
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
|
+
}
|
|
40101
41254
|
async function runListSharedSkillsTool(config2, options) {
|
|
40102
41255
|
try {
|
|
40103
41256
|
const result = await listSharedAgentArtifacts(config2, options);
|