threadnote 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +67 -80
- package/README.md +19 -2
- package/THIRD_PARTY.md +36 -0
- package/dist/mcp_server.cjs +566 -3
- package/dist/threadnote.cjs +429 -8
- package/docs/agent-instructions.md +22 -5
- package/docs/index.html +2 -2
- package/docs/share.md +100 -10
- package/package.json +4 -3
package/dist/threadnote.cjs
CHANGED
|
@@ -8457,6 +8457,9 @@ var import_node_path6 = require("node:path");
|
|
|
8457
8457
|
var TEAMS_FILE_VERSION = 1;
|
|
8458
8458
|
var SHARED_SEGMENT = "shared";
|
|
8459
8459
|
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
8460
|
+
var SHAREABLE_ARTIFACT_DIR = "agent-artifacts";
|
|
8461
|
+
var SHAREABLE_TOP_LEVEL_DIRS = [...SHAREABLE_MEMORY_KIND_DIRS, SHAREABLE_ARTIFACT_DIR];
|
|
8462
|
+
var ARTIFACT_INSTALL_METADATA_VERSION = 1;
|
|
8460
8463
|
var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
8461
8464
|
var DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
8462
8465
|
var SCRUBBER_PATTERNS = [
|
|
@@ -8548,7 +8551,7 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
8548
8551
|
if (!dryRun) {
|
|
8549
8552
|
await ensureSharedGitignore(worktree, git, options.push !== false);
|
|
8550
8553
|
const ingested = await ingestWorktreeFiles(config, newConfig, "create");
|
|
8551
|
-
console.log(`Ingested ${ingested} shared
|
|
8554
|
+
console.log(`Ingested ${ingested} shared file(s) into OpenViking.`);
|
|
8552
8555
|
}
|
|
8553
8556
|
}
|
|
8554
8557
|
var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
|
|
@@ -8875,7 +8878,7 @@ async function runShareSyncQuiet(config, state, options) {
|
|
|
8875
8878
|
return void 0;
|
|
8876
8879
|
}
|
|
8877
8880
|
async function stageShareableChanges(dryRun, git, worktree) {
|
|
8878
|
-
const pathspecs = [":(top)README.md", ":(top).gitignore", ...
|
|
8881
|
+
const pathspecs = [":(top)README.md", ":(top).gitignore", ...SHAREABLE_TOP_LEVEL_DIRS.map((dir) => `:(top)${dir}`)];
|
|
8879
8882
|
await maybeRun(dryRun, git, ["-C", worktree, "add", "--", ...pathspecs], { allowFailure: true });
|
|
8880
8883
|
}
|
|
8881
8884
|
async function publishShareGitChange(worktree, relativePath, commitMessage, options = {}) {
|
|
@@ -8997,6 +9000,157 @@ ${err instanceof Error ? err.message : String(err)}`,
|
|
|
8997
9000
|
}
|
|
8998
9001
|
console.log(`Published ${sourceUri} -> ${targetUri}`);
|
|
8999
9002
|
}
|
|
9003
|
+
async function runSharePublishArtifact(config, sourcePath, options) {
|
|
9004
|
+
const result = await shareAgentArtifact(config, sourcePath, options);
|
|
9005
|
+
printShareArtifactResult(result, options.preview === true);
|
|
9006
|
+
}
|
|
9007
|
+
async function shareAgentArtifact(config, sourcePath, options) {
|
|
9008
|
+
const team = await resolveTeam(config, options.team);
|
|
9009
|
+
const dryRun = options.dryRun === true;
|
|
9010
|
+
const preview = options.preview === true;
|
|
9011
|
+
const resolvedSourcePath = expandPath(sourcePath);
|
|
9012
|
+
if (!await isRegularFileNoSymlink(resolvedSourcePath)) {
|
|
9013
|
+
throw new Error(`Agent artifact source is not a regular file: ${resolvedSourcePath}`);
|
|
9014
|
+
}
|
|
9015
|
+
const artifact = inferShareArtifact(resolvedSourcePath, options);
|
|
9016
|
+
const rawContent = await (0, import_promises5.readFile)(resolvedSourcePath, "utf8");
|
|
9017
|
+
if (!rawContent.trim()) {
|
|
9018
|
+
throw new Error(`Refusing to share empty agent artifact: ${resolvedSourcePath}`);
|
|
9019
|
+
}
|
|
9020
|
+
const scrub = applyScrubber(rawContent, { redact: options.redact === true });
|
|
9021
|
+
const relativePath = sharedArtifactRelativePath(artifact);
|
|
9022
|
+
const targetPath = (0, import_node_path6.join)(team.config.worktree, ...relativePath.split("/"));
|
|
9023
|
+
const targetUri = workfileToVikingUri(config, team.config, targetPath);
|
|
9024
|
+
const messages = [
|
|
9025
|
+
`${preview ? "Previewing" : dryRun ? "Would share" : "Sharing"} ${artifact.kind} ${artifact.agent}/${artifact.name}`,
|
|
9026
|
+
`Source: ${portablePath(resolvedSourcePath)}`,
|
|
9027
|
+
`Destination: ${targetUri}`
|
|
9028
|
+
];
|
|
9029
|
+
if (preview) {
|
|
9030
|
+
if (scrub.blocker) {
|
|
9031
|
+
messages.push(`PREVIEW BLOCKED: ${scrub.blocker}. Strip the sensitive value or pass --redact.`);
|
|
9032
|
+
return {
|
|
9033
|
+
artifact,
|
|
9034
|
+
gitMessages: [],
|
|
9035
|
+
messages,
|
|
9036
|
+
sourcePath: resolvedSourcePath,
|
|
9037
|
+
targetPath,
|
|
9038
|
+
targetUri
|
|
9039
|
+
};
|
|
9040
|
+
}
|
|
9041
|
+
for (const redaction of scrub.redactions) {
|
|
9042
|
+
messages.push(`PREVIEW redact: ${redaction.count}\xD7 ${redaction.name}`);
|
|
9043
|
+
}
|
|
9044
|
+
return {
|
|
9045
|
+
artifact,
|
|
9046
|
+
gitMessages: [],
|
|
9047
|
+
messages,
|
|
9048
|
+
previewContent: scrub.cleaned,
|
|
9049
|
+
sourcePath: resolvedSourcePath,
|
|
9050
|
+
targetPath,
|
|
9051
|
+
targetUri
|
|
9052
|
+
};
|
|
9053
|
+
}
|
|
9054
|
+
if (scrub.blocker) {
|
|
9055
|
+
throw new Error(
|
|
9056
|
+
`Refusing to share ${resolvedSourcePath}: possible ${scrub.blocker}. Strip the sensitive value or pass --redact for soft-leak patterns.`
|
|
9057
|
+
);
|
|
9058
|
+
}
|
|
9059
|
+
for (const redaction of scrub.redactions) {
|
|
9060
|
+
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before sharing.`);
|
|
9061
|
+
}
|
|
9062
|
+
const content = scrub.cleaned;
|
|
9063
|
+
const existingContent = await readFileIfExists(targetPath) ?? void 0;
|
|
9064
|
+
if (existingContent !== void 0 && existingContent !== content && options.force !== true) {
|
|
9065
|
+
throw new Error(
|
|
9066
|
+
`Shared artifact already exists with different content: ${portablePath(targetPath)}. Pass --force to replace it.`
|
|
9067
|
+
);
|
|
9068
|
+
}
|
|
9069
|
+
if (dryRun) {
|
|
9070
|
+
messages.push(`Would write shared artifact: ${portablePath(targetPath)}`);
|
|
9071
|
+
}
|
|
9072
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
9073
|
+
const ovHasResource = !dryRun && await vikingResourceExists(ov, config, targetUri);
|
|
9074
|
+
await ensureSharedDirectoryChain(config, ov, targetUri, dryRun, { quiet: true });
|
|
9075
|
+
await writeMemoryFile(config, ov, targetUri, content, ovHasResource ? "replace" : "create", dryRun, { quiet: true });
|
|
9076
|
+
const message = options.message ?? `share: publish ${relativePath}`;
|
|
9077
|
+
const gitMessages = await publishShareGitChange(team.config.worktree, relativePath, message, {
|
|
9078
|
+
dryRun,
|
|
9079
|
+
push: options.push
|
|
9080
|
+
});
|
|
9081
|
+
return { artifact, gitMessages, messages, sourcePath: resolvedSourcePath, targetPath, targetUri };
|
|
9082
|
+
}
|
|
9083
|
+
async function runShareInstallArtifacts(config, options) {
|
|
9084
|
+
const result = await installSharedAgentArtifacts(config, options);
|
|
9085
|
+
if (result.syncedTeams.length > 0) {
|
|
9086
|
+
console.log(`Synced shared teams: ${result.syncedTeams.join(", ")}`);
|
|
9087
|
+
}
|
|
9088
|
+
for (const warning2 of result.warnings) {
|
|
9089
|
+
console.warn(`Warning: ${warning2}`);
|
|
9090
|
+
}
|
|
9091
|
+
for (const message of result.messages) {
|
|
9092
|
+
console.log(message);
|
|
9093
|
+
}
|
|
9094
|
+
}
|
|
9095
|
+
async function installSharedAgentArtifacts(config, options) {
|
|
9096
|
+
const syncResult = await maybeSyncSharedArtifacts(config, options);
|
|
9097
|
+
const team = await resolveTeam(config, options.team);
|
|
9098
|
+
const dryRun = options.dryRun === true || options.apply !== true;
|
|
9099
|
+
const allArtifacts = await collectSharedArtifacts(team.config.worktree, team.name);
|
|
9100
|
+
const artifacts = filterSharedArtifacts(allArtifacts, options);
|
|
9101
|
+
const messages = [];
|
|
9102
|
+
if (artifacts.length === 0) {
|
|
9103
|
+
const filters = sharedArtifactFilterLabel(options);
|
|
9104
|
+
if (filters) {
|
|
9105
|
+
throw new Error(`No shared agent artifacts found for team "${team.name}" matching ${filters}.`);
|
|
9106
|
+
}
|
|
9107
|
+
return {
|
|
9108
|
+
installedCount: 0,
|
|
9109
|
+
messages: [`No shared agent artifacts found for team "${team.name}".`],
|
|
9110
|
+
syncedTeams: syncResult.syncedTeams,
|
|
9111
|
+
team: team.name,
|
|
9112
|
+
warnings: syncResult.warnings
|
|
9113
|
+
};
|
|
9114
|
+
}
|
|
9115
|
+
if (options.name !== void 0 && artifacts.length > 1 && (options.agent === void 0 || options.kind === void 0)) {
|
|
9116
|
+
throw new Error(
|
|
9117
|
+
`Shared artifact "${options.name}" is ambiguous. Specify agent and kind. Matches: ${artifacts.map((artifact) => sharedArtifactLabel(artifact.artifact)).join(", ")}`
|
|
9118
|
+
);
|
|
9119
|
+
}
|
|
9120
|
+
let installedCount = 0;
|
|
9121
|
+
for (const artifact of artifacts) {
|
|
9122
|
+
const label = sharedArtifactLabel(artifact.artifact);
|
|
9123
|
+
const state = await sharedArtifactInstallState(artifact);
|
|
9124
|
+
if (dryRun) {
|
|
9125
|
+
const verb = sharedArtifactDryRunVerb(state.status, options.force === true);
|
|
9126
|
+
const suffix = sharedArtifactDryRunSuffix(state.status, options.force === true);
|
|
9127
|
+
messages.push(`${verb} ${label}: ${portablePath(artifact.installPath)}${suffix}`);
|
|
9128
|
+
continue;
|
|
9129
|
+
}
|
|
9130
|
+
if ((state.status === "local_modified" || state.status === "remote_changed_and_local_modified") && options.force !== true) {
|
|
9131
|
+
throw new Error(`Refusing to overwrite ${portablePath(artifact.installPath)}. Pass force=true or --force.`);
|
|
9132
|
+
}
|
|
9133
|
+
if (state.status === "current") {
|
|
9134
|
+
await writeSharedArtifactMetadata(artifact, state.sourceSha);
|
|
9135
|
+
messages.push(`Already installed ${label}: ${portablePath(artifact.installPath)}`);
|
|
9136
|
+
continue;
|
|
9137
|
+
}
|
|
9138
|
+
await ensureDirectory((0, import_node_path6.dirname)(artifact.installPath), false);
|
|
9139
|
+
await (0, import_promises5.writeFile)(artifact.installPath, state.sourceContent, { encoding: "utf8", mode: 384 });
|
|
9140
|
+
await writeSharedArtifactMetadata(artifact, state.sourceSha);
|
|
9141
|
+
installedCount += 1;
|
|
9142
|
+
messages.push(
|
|
9143
|
+
`${sharedArtifactInstallVerb(state.status, options.force === true)} ${label}: ${portablePath(artifact.installPath)}`
|
|
9144
|
+
);
|
|
9145
|
+
}
|
|
9146
|
+
return {
|
|
9147
|
+
installedCount,
|
|
9148
|
+
messages,
|
|
9149
|
+
syncedTeams: syncResult.syncedTeams,
|
|
9150
|
+
team: team.name,
|
|
9151
|
+
warnings: syncResult.warnings
|
|
9152
|
+
};
|
|
9153
|
+
}
|
|
9000
9154
|
async function runShareUnpublish(config, sourceUri, options) {
|
|
9001
9155
|
assertVikingUri(sourceUri);
|
|
9002
9156
|
const team = await resolveTeam(config, options.team);
|
|
@@ -9089,7 +9243,7 @@ async function runShareRename(config, options) {
|
|
|
9089
9243
|
console.log(`Would rename worktree: ${portablePath(oldTeam.config.worktree)} -> ${portablePath(newWorktree)}`);
|
|
9090
9244
|
console.log(`Would rename gitdir: ${portablePath(oldTeam.config.gitdir)} -> ${portablePath(newGitdir)}`);
|
|
9091
9245
|
console.log(`Would update git core.worktree for team "${newName}".`);
|
|
9092
|
-
console.log(`Would reindex shared
|
|
9246
|
+
console.log(`Would reindex shared context under team "${newName}" and remove old shared URI tree.`);
|
|
9093
9247
|
console.log(`Would write teams file: ${teamsFilePath(config)}`);
|
|
9094
9248
|
return;
|
|
9095
9249
|
}
|
|
@@ -9107,7 +9261,7 @@ async function runShareRename(config, options) {
|
|
|
9107
9261
|
false
|
|
9108
9262
|
);
|
|
9109
9263
|
console.log(`Renamed shared team "${oldTeam.name}" -> "${newName}".`);
|
|
9110
|
-
console.log(`Reindexed ${ingested} shared
|
|
9264
|
+
console.log(`Reindexed ${ingested} shared file(s).`);
|
|
9111
9265
|
}
|
|
9112
9266
|
async function runShareSetUrl(config, remoteUrl, options) {
|
|
9113
9267
|
if (!remoteUrl.trim()) {
|
|
@@ -9350,7 +9504,7 @@ async function walkMemoryFiles(root) {
|
|
|
9350
9504
|
}
|
|
9351
9505
|
const full = (0, import_node_path6.join)(path, entry.name);
|
|
9352
9506
|
if (entry.isDirectory()) {
|
|
9353
|
-
if (depth === 0 && !
|
|
9507
|
+
if (depth === 0 && !SHAREABLE_TOP_LEVEL_DIRS.includes(entry.name)) {
|
|
9354
9508
|
continue;
|
|
9355
9509
|
}
|
|
9356
9510
|
await visit(full, depth + 1);
|
|
@@ -9432,6 +9586,264 @@ function vikingUriToWorktreeRelative(config, uri, team) {
|
|
|
9432
9586
|
}
|
|
9433
9587
|
return uri.slice(prefix.length);
|
|
9434
9588
|
}
|
|
9589
|
+
async function isRegularFileNoSymlink(path) {
|
|
9590
|
+
try {
|
|
9591
|
+
const stat5 = await (0, import_promises5.lstat)(path);
|
|
9592
|
+
return stat5.isFile();
|
|
9593
|
+
} catch (_err) {
|
|
9594
|
+
return false;
|
|
9595
|
+
}
|
|
9596
|
+
}
|
|
9597
|
+
function inferShareArtifact(path, options) {
|
|
9598
|
+
const normalizedPath = path.split(import_node_path6.sep).join("/");
|
|
9599
|
+
const fileName = (0, import_node_path6.basename)(path);
|
|
9600
|
+
const lowerFileName = fileName.toLowerCase();
|
|
9601
|
+
const lowerPath = normalizedPath.toLowerCase();
|
|
9602
|
+
const inferredKind = lowerFileName === "skill.md" ? "skill" : lowerPath.includes("/.claude/commands/") && lowerFileName.endsWith(".md") ? "command" : void 0;
|
|
9603
|
+
const inferredAgent = lowerPath.includes("/.codex/skills/") ? "codex" : lowerPath.includes("/.claude/skills/") || lowerPath.includes("/.claude/commands/") ? "claude" : void 0;
|
|
9604
|
+
const extensionIndex = fileName.lastIndexOf(".");
|
|
9605
|
+
const stem = extensionIndex > 0 ? fileName.slice(0, extensionIndex) : fileName;
|
|
9606
|
+
const inferredName = lowerFileName === "skill.md" ? (0, import_node_path6.basename)((0, import_node_path6.dirname)(path)) : stem;
|
|
9607
|
+
const kind = options.kind ?? inferredKind;
|
|
9608
|
+
const agent = options.agent ?? inferredAgent;
|
|
9609
|
+
const name = options.name ?? inferredName;
|
|
9610
|
+
if (kind !== "skill" && kind !== "command") {
|
|
9611
|
+
throw new Error("Could not infer artifact kind. Pass --kind skill or --kind command.");
|
|
9612
|
+
}
|
|
9613
|
+
if (agent !== "codex" && agent !== "claude") {
|
|
9614
|
+
throw new Error("Could not infer artifact agent. Pass --agent codex or --agent claude.");
|
|
9615
|
+
}
|
|
9616
|
+
if (kind === "skill" && lowerFileName !== "skill.md") {
|
|
9617
|
+
throw new Error("Skill artifacts must point at a SKILL.md file.");
|
|
9618
|
+
}
|
|
9619
|
+
if (kind === "command" && !lowerFileName.endsWith(".md")) {
|
|
9620
|
+
throw new Error("Command artifacts must be Markdown files.");
|
|
9621
|
+
}
|
|
9622
|
+
if (kind === "command" && agent !== "claude") {
|
|
9623
|
+
throw new Error("Only Claude command artifacts are supported.");
|
|
9624
|
+
}
|
|
9625
|
+
if (name.trim().length === 0) {
|
|
9626
|
+
throw new Error("Artifact name cannot be empty.");
|
|
9627
|
+
}
|
|
9628
|
+
return { agent, kind, name: uriSegment(name) };
|
|
9629
|
+
}
|
|
9630
|
+
function sharedArtifactRelativePath(artifact) {
|
|
9631
|
+
if (artifact.kind === "skill") {
|
|
9632
|
+
return `${SHAREABLE_ARTIFACT_DIR}/skills/${artifact.agent}/${artifact.name}/SKILL.md`;
|
|
9633
|
+
}
|
|
9634
|
+
return `${SHAREABLE_ARTIFACT_DIR}/commands/${artifact.agent}/${artifact.name}.md`;
|
|
9635
|
+
}
|
|
9636
|
+
function sharedArtifactFromRelativePath(relativePath) {
|
|
9637
|
+
const parts = relativePath.split("/");
|
|
9638
|
+
if (parts[0] !== SHAREABLE_ARTIFACT_DIR) {
|
|
9639
|
+
return void 0;
|
|
9640
|
+
}
|
|
9641
|
+
if (parts.length === 5 && parts[1] === "skills" && (parts[2] === "codex" || parts[2] === "claude") && parts[4] === "SKILL.md") {
|
|
9642
|
+
return { agent: parts[2], kind: "skill", name: parts[3] };
|
|
9643
|
+
}
|
|
9644
|
+
if (parts.length === 4 && parts[1] === "commands" && parts[2] === "claude" && parts[3].endsWith(".md")) {
|
|
9645
|
+
return { agent: "claude", kind: "command", name: parts[3].slice(0, -".md".length) };
|
|
9646
|
+
}
|
|
9647
|
+
return void 0;
|
|
9648
|
+
}
|
|
9649
|
+
async function collectSharedArtifacts(worktree, team) {
|
|
9650
|
+
const root = (0, import_node_path6.join)(worktree, SHAREABLE_ARTIFACT_DIR);
|
|
9651
|
+
if (!await isDirectory(root)) {
|
|
9652
|
+
return [];
|
|
9653
|
+
}
|
|
9654
|
+
const out = [];
|
|
9655
|
+
async function visit(path) {
|
|
9656
|
+
const entries = await (0, import_promises5.readdir)(path, { withFileTypes: true });
|
|
9657
|
+
for (const entry of entries) {
|
|
9658
|
+
const full = (0, import_node_path6.join)(path, entry.name);
|
|
9659
|
+
if (entry.isDirectory()) {
|
|
9660
|
+
await visit(full);
|
|
9661
|
+
continue;
|
|
9662
|
+
}
|
|
9663
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
9664
|
+
continue;
|
|
9665
|
+
}
|
|
9666
|
+
const relativePath = (0, import_node_path6.relative)(worktree, full).split(import_node_path6.sep).join("/");
|
|
9667
|
+
const artifact = sharedArtifactFromRelativePath(relativePath);
|
|
9668
|
+
if (artifact === void 0) {
|
|
9669
|
+
continue;
|
|
9670
|
+
}
|
|
9671
|
+
out.push({
|
|
9672
|
+
artifact,
|
|
9673
|
+
installPath: sharedArtifactInstallPath(team, artifact),
|
|
9674
|
+
sourcePath: full,
|
|
9675
|
+
sourceRelativePath: relativePath,
|
|
9676
|
+
team
|
|
9677
|
+
});
|
|
9678
|
+
}
|
|
9679
|
+
}
|
|
9680
|
+
await visit(root);
|
|
9681
|
+
return out.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
|
|
9682
|
+
}
|
|
9683
|
+
function filterSharedArtifacts(artifacts, options) {
|
|
9684
|
+
const name = options.name === void 0 ? void 0 : uriSegment(options.name);
|
|
9685
|
+
return artifacts.filter((artifact) => {
|
|
9686
|
+
if (options.agent !== void 0 && artifact.artifact.agent !== options.agent) {
|
|
9687
|
+
return false;
|
|
9688
|
+
}
|
|
9689
|
+
if (options.kind !== void 0 && artifact.artifact.kind !== options.kind) {
|
|
9690
|
+
return false;
|
|
9691
|
+
}
|
|
9692
|
+
if (name !== void 0 && artifact.artifact.name !== name) {
|
|
9693
|
+
return false;
|
|
9694
|
+
}
|
|
9695
|
+
return true;
|
|
9696
|
+
});
|
|
9697
|
+
}
|
|
9698
|
+
async function maybeSyncSharedArtifacts(config, options) {
|
|
9699
|
+
if (options.sync === false) {
|
|
9700
|
+
return { syncedTeams: [], warnings: [] };
|
|
9701
|
+
}
|
|
9702
|
+
return syncSharedReposBeforeAgentRead(config);
|
|
9703
|
+
}
|
|
9704
|
+
async function sharedArtifactInstallState(artifact) {
|
|
9705
|
+
const sourceContent = await (0, import_promises5.readFile)(artifact.sourcePath, "utf8");
|
|
9706
|
+
const sourceSha = sha256(sourceContent);
|
|
9707
|
+
const existingContent = await readFileIfExists(artifact.installPath) ?? void 0;
|
|
9708
|
+
if (existingContent === void 0) {
|
|
9709
|
+
return { sourceContent, sourceSha, status: "not_installed" };
|
|
9710
|
+
}
|
|
9711
|
+
const existingSha = sha256(existingContent);
|
|
9712
|
+
const metadata = await readSharedArtifactMetadata(artifact);
|
|
9713
|
+
if (existingSha === sourceSha) {
|
|
9714
|
+
return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "current" };
|
|
9715
|
+
}
|
|
9716
|
+
if (metadata === void 0) {
|
|
9717
|
+
return { existingContent, existingSha, sourceContent, sourceSha, status: "local_modified" };
|
|
9718
|
+
}
|
|
9719
|
+
const remoteChanged = metadata.sourceSha256 !== sourceSha;
|
|
9720
|
+
const localChanged = metadata.installedSha256 !== existingSha;
|
|
9721
|
+
if (remoteChanged && localChanged) {
|
|
9722
|
+
return {
|
|
9723
|
+
existingContent,
|
|
9724
|
+
existingSha,
|
|
9725
|
+
metadata,
|
|
9726
|
+
sourceContent,
|
|
9727
|
+
sourceSha,
|
|
9728
|
+
status: "remote_changed_and_local_modified"
|
|
9729
|
+
};
|
|
9730
|
+
}
|
|
9731
|
+
if (remoteChanged) {
|
|
9732
|
+
return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "update_available" };
|
|
9733
|
+
}
|
|
9734
|
+
return { existingContent, existingSha, metadata, sourceContent, sourceSha, status: "local_modified" };
|
|
9735
|
+
}
|
|
9736
|
+
async function readSharedArtifactMetadata(artifact) {
|
|
9737
|
+
const raw = await readFileIfExists(sharedArtifactMetadataPath(artifact));
|
|
9738
|
+
if (raw === void 0) {
|
|
9739
|
+
return void 0;
|
|
9740
|
+
}
|
|
9741
|
+
const parsed = parseJsonConfigObject(raw);
|
|
9742
|
+
if (parsed === void 0 || parsed.version !== ARTIFACT_INSTALL_METADATA_VERSION) {
|
|
9743
|
+
return void 0;
|
|
9744
|
+
}
|
|
9745
|
+
const artifactValue = parsed.artifact;
|
|
9746
|
+
if (typeof parsed.team !== "string" || typeof parsed.source !== "string" || typeof parsed.sourceSha256 !== "string" || typeof parsed.installedSha256 !== "string" || typeof parsed.installedAt !== "string" || typeof artifactValue !== "object" || artifactValue === null || Array.isArray(artifactValue)) {
|
|
9747
|
+
return void 0;
|
|
9748
|
+
}
|
|
9749
|
+
const metadataArtifact = artifactValue;
|
|
9750
|
+
if (metadataArtifact.agent !== artifact.artifact.agent || metadataArtifact.kind !== artifact.artifact.kind || metadataArtifact.name !== artifact.artifact.name) {
|
|
9751
|
+
return void 0;
|
|
9752
|
+
}
|
|
9753
|
+
return {
|
|
9754
|
+
artifact: artifact.artifact,
|
|
9755
|
+
installedAt: parsed.installedAt,
|
|
9756
|
+
installedSha256: parsed.installedSha256,
|
|
9757
|
+
source: parsed.source,
|
|
9758
|
+
sourceSha256: parsed.sourceSha256,
|
|
9759
|
+
team: parsed.team,
|
|
9760
|
+
version: ARTIFACT_INSTALL_METADATA_VERSION
|
|
9761
|
+
};
|
|
9762
|
+
}
|
|
9763
|
+
async function writeSharedArtifactMetadata(artifact, sourceSha) {
|
|
9764
|
+
const metadata = {
|
|
9765
|
+
artifact: artifact.artifact,
|
|
9766
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9767
|
+
installedSha256: sourceSha,
|
|
9768
|
+
source: artifact.sourceRelativePath,
|
|
9769
|
+
sourceSha256: sourceSha,
|
|
9770
|
+
team: artifact.team,
|
|
9771
|
+
version: ARTIFACT_INSTALL_METADATA_VERSION
|
|
9772
|
+
};
|
|
9773
|
+
const metadataPath = sharedArtifactMetadataPath(artifact);
|
|
9774
|
+
await ensureDirectory((0, import_node_path6.dirname)(metadataPath), false);
|
|
9775
|
+
await (0, import_promises5.writeFile)(metadataPath, `${JSON.stringify(metadata, void 0, 2)}
|
|
9776
|
+
`, { encoding: "utf8", mode: 384 });
|
|
9777
|
+
}
|
|
9778
|
+
function sharedArtifactMetadataPath(artifact) {
|
|
9779
|
+
return `${artifact.installPath}.threadnote-install.json`;
|
|
9780
|
+
}
|
|
9781
|
+
function sharedArtifactDryRunVerb(status, force) {
|
|
9782
|
+
switch (status) {
|
|
9783
|
+
case "not_installed":
|
|
9784
|
+
return "Would install";
|
|
9785
|
+
case "current":
|
|
9786
|
+
return "Already installed";
|
|
9787
|
+
case "update_available":
|
|
9788
|
+
return "Would update";
|
|
9789
|
+
case "local_modified":
|
|
9790
|
+
case "remote_changed_and_local_modified":
|
|
9791
|
+
return force ? "Would replace" : "Would skip modified";
|
|
9792
|
+
}
|
|
9793
|
+
}
|
|
9794
|
+
function sharedArtifactDryRunSuffix(status, force) {
|
|
9795
|
+
if ((status === "local_modified" || status === "remote_changed_and_local_modified") && !force) {
|
|
9796
|
+
return " (pass --force to replace local changes)";
|
|
9797
|
+
}
|
|
9798
|
+
return "";
|
|
9799
|
+
}
|
|
9800
|
+
function sharedArtifactInstallVerb(status, force) {
|
|
9801
|
+
if (force && (status === "local_modified" || status === "remote_changed_and_local_modified")) {
|
|
9802
|
+
return "Replaced";
|
|
9803
|
+
}
|
|
9804
|
+
if (status === "update_available") {
|
|
9805
|
+
return "Updated";
|
|
9806
|
+
}
|
|
9807
|
+
return "Installed";
|
|
9808
|
+
}
|
|
9809
|
+
function sharedArtifactFilterLabel(options) {
|
|
9810
|
+
const filters = [];
|
|
9811
|
+
if (options.kind !== void 0) {
|
|
9812
|
+
filters.push(`kind=${options.kind}`);
|
|
9813
|
+
}
|
|
9814
|
+
if (options.agent !== void 0) {
|
|
9815
|
+
filters.push(`agent=${options.agent}`);
|
|
9816
|
+
}
|
|
9817
|
+
if (options.name !== void 0) {
|
|
9818
|
+
filters.push(`name=${uriSegment(options.name)}`);
|
|
9819
|
+
}
|
|
9820
|
+
return filters.join(", ");
|
|
9821
|
+
}
|
|
9822
|
+
function sharedArtifactLabel(artifact) {
|
|
9823
|
+
return `${artifact.kind} ${artifact.agent}/${artifact.name}`;
|
|
9824
|
+
}
|
|
9825
|
+
function sharedArtifactInstallPath(team, artifact) {
|
|
9826
|
+
if (artifact.kind === "skill" && artifact.agent === "codex") {
|
|
9827
|
+
return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".codex", "skills", "threadnote", team, artifact.name, "SKILL.md");
|
|
9828
|
+
}
|
|
9829
|
+
if (artifact.kind === "skill" && artifact.agent === "claude") {
|
|
9830
|
+
return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".claude", "skills", "threadnote", team, artifact.name, "SKILL.md");
|
|
9831
|
+
}
|
|
9832
|
+
return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".claude", "commands", "threadnote", team, `${artifact.name}.md`);
|
|
9833
|
+
}
|
|
9834
|
+
function printShareArtifactResult(result, preview) {
|
|
9835
|
+
for (const message of result.messages) {
|
|
9836
|
+
console.log(message);
|
|
9837
|
+
}
|
|
9838
|
+
for (const gitMessage of result.gitMessages) {
|
|
9839
|
+
console.log(gitMessage);
|
|
9840
|
+
}
|
|
9841
|
+
if (preview && result.previewContent !== void 0) {
|
|
9842
|
+
console.log("-----BEGIN PREVIEW-----");
|
|
9843
|
+
console.log(result.previewContent);
|
|
9844
|
+
console.log("-----END PREVIEW-----");
|
|
9845
|
+
}
|
|
9846
|
+
}
|
|
9435
9847
|
function stripPersonalProvenance(content) {
|
|
9436
9848
|
const lines = content.split("\n");
|
|
9437
9849
|
let headerEnd = lines.length;
|
|
@@ -9480,9 +9892,9 @@ async function ensureSharedDirectoryChain(config, ov, memoryUri, dryRun, options
|
|
|
9480
9892
|
continue;
|
|
9481
9893
|
}
|
|
9482
9894
|
if (options.quiet === true) {
|
|
9483
|
-
await runCommand(ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared
|
|
9895
|
+
await runCommand(ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared context."]));
|
|
9484
9896
|
} else {
|
|
9485
|
-
await maybeRun(false, ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared
|
|
9897
|
+
await maybeRun(false, ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared context."]));
|
|
9486
9898
|
}
|
|
9487
9899
|
}
|
|
9488
9900
|
}
|
|
@@ -9711,7 +10123,7 @@ async function applyChangesToOpenViking(config, team, changes, options = {}) {
|
|
|
9711
10123
|
continue;
|
|
9712
10124
|
}
|
|
9713
10125
|
const firstSegment = change.relativePath.split("/")[0];
|
|
9714
|
-
if (!
|
|
10126
|
+
if (!SHAREABLE_TOP_LEVEL_DIRS.includes(firstSegment)) {
|
|
9715
10127
|
continue;
|
|
9716
10128
|
}
|
|
9717
10129
|
const uri = workfileToVikingUri(config, team, change.path);
|
|
@@ -14821,6 +15233,15 @@ async function main() {
|
|
|
14821
15233
|
).action(async (uri, options) => {
|
|
14822
15234
|
await runSharePublish(getRuntimeConfig(program2), uri, options);
|
|
14823
15235
|
});
|
|
15236
|
+
share.command("publish-artifact").description("Publish a local Codex/Claude skill or Claude command into the shared team repo").argument("<path>", "Path to SKILL.md or a Claude command markdown file").option("--team <name>", "Team name; defaults to the configured default team").option("--agent <agent>", "Agent owner when path inference is ambiguous: codex or claude").option("--kind <kind>", "Artifact kind when path inference is ambiguous: skill or command").option("--name <name>", "Shared artifact name; defaults to skill directory or command file stem").option("--message <text>", "Commit message override").option("--force", "Replace an existing shared artifact with different content").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").option("--preview", "Print the exact artifact bytes that would land in the shared git repo").option(
|
|
15237
|
+
"--redact",
|
|
15238
|
+
"Replace soft-leak matches (local paths) with placeholders and continue; credentials still block"
|
|
15239
|
+
).action(async (path, options) => {
|
|
15240
|
+
await runSharePublishArtifact(getRuntimeConfig(program2), path, options);
|
|
15241
|
+
});
|
|
15242
|
+
share.command("install-artifacts").description("Install shared Codex/Claude skills and Claude commands from a team repo").option("--team <name>", "Team name; defaults to the configured default team").option("--agent <agent>", "Install only artifacts for this agent: codex or claude").option("--kind <kind>", "Install only artifacts of this kind: skill or command").option("--name <name>", "Install only this shared artifact name").option("--apply", "Actually write files into local agent skill/command directories").option("--force", "Replace existing installed artifacts with different content").option("--no-sync", "Skip pulling shared team updates before listing/installing artifacts").option("--dry-run", "Preview install actions without writing files").action(async (options) => {
|
|
15243
|
+
await runShareInstallArtifacts(getRuntimeConfig(program2), options);
|
|
15244
|
+
});
|
|
14824
15245
|
share.command("unpublish").description("Pull a shared memory back into the personal namespace, commit removal and push").argument("<viking-uri>", "viking:// memory URI inside a team shared subtree").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").action(async (uri, options) => {
|
|
14825
15246
|
await runShareUnpublish(getRuntimeConfig(program2), uri, options);
|
|
14826
15247
|
});
|
|
@@ -119,11 +119,12 @@ compact_context({"project":"my-repo","topic":"active-bug","dryRun":true})
|
|
|
119
119
|
|
|
120
120
|
Never compact secrets, credentials, customer data, raw production logs, or checked-in canonical docs into memory.
|
|
121
121
|
|
|
122
|
-
## Sharing
|
|
122
|
+
## Sharing context with teammates
|
|
123
123
|
|
|
124
|
-
Threadnote can publish a curated subset of durable memories
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
Threadnote can publish a curated subset of durable memories and agent artifacts (Codex/Claude skills and Claude
|
|
125
|
+
commands) into a team git repo so other engineers' agents can pull them. The mechanism lives under the
|
|
126
|
+
`viking://user/<you>/memories/shared/<team>/...` subtree; only content that is explicitly published leaves the machine.
|
|
127
|
+
Personal handoffs, preferences, and unpublished durable notes always stay local.
|
|
127
128
|
|
|
128
129
|
Publish a durable memory when its content is useful to other engineers working on the same project (intended behavior,
|
|
129
130
|
design decisions, API contracts, gotchas) and is safe to share. Do NOT publish:
|
|
@@ -136,6 +137,16 @@ The MCP tool `share_publish` runs the same scrubber as the CLI and refuses to pu
|
|
|
136
137
|
patterns (PEM private keys, `sk-...`, `gh[pousr]_...`, `Bearer ...`, `AKIA...`, `xox[abprs]-...`). It writes and pushes
|
|
137
138
|
the shared copy first, then removes the personal copy after the push succeeds.
|
|
138
139
|
|
|
140
|
+
The MCP tool `share_skill` shares a local Codex/Claude `SKILL.md` file or Claude command markdown file into the team's
|
|
141
|
+
`agent-artifacts/` catalog after the same scrubber checks. Shared artifacts are recallable after sync, but local
|
|
142
|
+
installation remains opt-in with `threadnote share install-artifacts --apply`.
|
|
143
|
+
|
|
144
|
+
When the user asks whether shared skills exist, call `list_shared_skills`. When the user asks to install one, call
|
|
145
|
+
`install_shared_skill` with the selected `name`; include `agent` and `kind` from the list result when available.
|
|
146
|
+
`list_shared_skills` syncs shared repos before listing and reports `not_installed`, `current`, `update_available`,
|
|
147
|
+
`local_modified`, or `remote_changed_and_local_modified`. Install/update selected shared skills only when the user asks;
|
|
148
|
+
use `force:true` only when the user explicitly accepts overwriting local modifications.
|
|
149
|
+
|
|
139
150
|
Incoming shared memories are normally fetched and synced automatically before MCP `recall_context` / `read_context` and
|
|
140
151
|
CLI `threadnote recall` / `threadnote read` return. If automatic sync reports a dirty worktree, a conflict, or another
|
|
141
152
|
git issue, run `threadnote share sync` after resolving the local state to pull, reindex, and push explicitly.
|
|
@@ -144,9 +155,13 @@ git issue, run `threadnote share sync` after resolving the local state to pull,
|
|
|
144
155
|
# MCP call shape
|
|
145
156
|
share_publish({"uri":"viking://user/you/memories/durable/projects/foo/bar.md"})
|
|
146
157
|
share_publish({"uri":"viking://user/you/memories/durable/projects/foo/bar.md","team":"friends","push":false})
|
|
158
|
+
share_skill({"path":"~/.codex/skills/reviewer/SKILL.md"})
|
|
159
|
+
list_shared_skills({})
|
|
160
|
+
install_shared_skill({"name":"reviewer","agent":"codex","kind":"skill"})
|
|
147
161
|
```
|
|
148
162
|
|
|
149
|
-
Before publishing, confirm with the user unless they have already instructed you to share durable memories
|
|
163
|
+
Before publishing, confirm with the user unless they have already instructed you to share durable memories or agent
|
|
164
|
+
artifacts autonomously.
|
|
150
165
|
|
|
151
166
|
## Handoff
|
|
152
167
|
|
|
@@ -184,5 +199,7 @@ threadnote forget viking://user/example/memories/events/duplicate.md
|
|
|
184
199
|
threadnote handoff --project example --topic active-issue --task "short task summary" --tests "checks run" --next-step "what to do next"
|
|
185
200
|
threadnote share init git@github.com:org/team-memories.git
|
|
186
201
|
threadnote share publish viking://user/example/memories/durable/projects/foo/bar.md
|
|
202
|
+
threadnote share publish-artifact ~/.codex/skills/example/SKILL.md
|
|
203
|
+
threadnote share install-artifacts --name example --agent codex --kind skill --apply
|
|
187
204
|
threadnote share sync
|
|
188
205
|
```
|
package/docs/index.html
CHANGED
|
@@ -783,7 +783,7 @@
|
|
|
783
783
|
team-shared decisions — recallable from any session, on any agent, in any worktree.
|
|
784
784
|
</p>
|
|
785
785
|
<div class="cta-row">
|
|
786
|
-
<span class="pill"><span id="version-tag">v0.6.1</span> · local-first ·
|
|
786
|
+
<span class="pill"><span id="version-tag">v0.6.1</span> · local-first · AGPL-3.0-or-later</span>
|
|
787
787
|
</div>
|
|
788
788
|
</div>
|
|
789
789
|
<div class="scroll-cue">scroll ↓ · <span class="kbd">↓</span> / <span class="kbd">j</span></div>
|
|
@@ -1585,7 +1585,7 @@ threadnote doctor --dry-run</code></pre>
|
|
|
1585
1585
|
</div>
|
|
1586
1586
|
|
|
1587
1587
|
<p class="colophon">
|
|
1588
|
-
threadnote ·
|
|
1588
|
+
threadnote · AGPL-3.0-or-later · built on OpenViking 0.3.24 · use <span class="kbd">↑</span>
|
|
1589
1589
|
<span class="kbd">↓</span> / <span class="kbd">j</span> <span class="kbd">k</span> to navigate
|
|
1590
1590
|
</p>
|
|
1591
1591
|
</div>
|