threadnote 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/mcp_server.cjs +4082 -999
- package/dist/threadnote.cjs +856 -69
- package/docs/agent-instructions.md +34 -3
- package/docs/share.md +151 -0
- package/package.json +1 -1
package/dist/threadnote.cjs
CHANGED
|
@@ -7382,9 +7382,9 @@ async function runRead(config, uri, options) {
|
|
|
7382
7382
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
7383
7383
|
const result = await maybeRun(options.dryRun === true, ov, withIdentity(config, ["read", uri]));
|
|
7384
7384
|
if (result && result.stdout.includes("[Directory overview is not ready]") && (uri.endsWith("/.overview.md") || uri.endsWith("/.abstract.md"))) {
|
|
7385
|
-
const
|
|
7385
|
+
const parentUri2 = parentVikingUri(uri);
|
|
7386
7386
|
console.log("\nThis is a generated summary placeholder. To read the underlying content, inspect leaf nodes:");
|
|
7387
|
-
console.log(` threadnote list ${
|
|
7387
|
+
console.log(` threadnote list ${parentUri2} --all --recursive`);
|
|
7388
7388
|
}
|
|
7389
7389
|
}
|
|
7390
7390
|
async function runList(config, uri, options) {
|
|
@@ -7727,6 +7727,7 @@ function exactMemoryScopes(config, includeArchived) {
|
|
|
7727
7727
|
`${userBase}/handoffs/active`,
|
|
7728
7728
|
`${userBase}/incidents/active`,
|
|
7729
7729
|
`${userBase}/events`,
|
|
7730
|
+
`${userBase}/shared`,
|
|
7730
7731
|
`viking://agent/${uriSegment(config.agentId)}/memories`
|
|
7731
7732
|
];
|
|
7732
7733
|
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
@@ -8395,19 +8396,780 @@ function detectSecretMatches(content) {
|
|
|
8395
8396
|
return matches;
|
|
8396
8397
|
}
|
|
8397
8398
|
|
|
8399
|
+
// src/share.ts
|
|
8400
|
+
var import_promises6 = require("node:fs/promises");
|
|
8401
|
+
var import_node_os4 = require("node:os");
|
|
8402
|
+
var import_node_path6 = require("node:path");
|
|
8403
|
+
var TEAMS_FILE_VERSION = 1;
|
|
8404
|
+
var SHARED_SEGMENT = "shared";
|
|
8405
|
+
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
8406
|
+
var DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
8407
|
+
var SCRUBBER_PATTERNS = [
|
|
8408
|
+
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
8409
|
+
{ name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
8410
|
+
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
8411
|
+
{ name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
|
|
8412
|
+
{ name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
|
|
8413
|
+
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
8414
|
+
// Matches bare JWTs (three base64url segments). May surface a JWE token in
|
|
8415
|
+
// legitimate docs; if that becomes noisy we can switch to warn-only.
|
|
8416
|
+
{ name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
8417
|
+
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
8418
|
+
// Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
|
|
8419
|
+
// xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
|
|
8420
|
+
{ name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i }
|
|
8421
|
+
];
|
|
8422
|
+
async function runShareInit(config, remoteUrl, options) {
|
|
8423
|
+
if (!remoteUrl.trim()) {
|
|
8424
|
+
throw new Error("Provide a git remote URL for the shared memories repo.");
|
|
8425
|
+
}
|
|
8426
|
+
const dryRun = options.dryRun === true;
|
|
8427
|
+
const teamName = normalizeTeamName(options.team);
|
|
8428
|
+
const teamsFile = await readTeamsFile(config);
|
|
8429
|
+
if (teamsFile.teams[teamName]) {
|
|
8430
|
+
throw new Error(
|
|
8431
|
+
`Team "${teamName}" is already configured (remote ${teamsFile.teams[teamName].remote}). Remove it first with: threadnote share remove --team ${teamName}`
|
|
8432
|
+
);
|
|
8433
|
+
}
|
|
8434
|
+
const worktree = teamWorktreePath(config, teamName);
|
|
8435
|
+
const gitdir = teamGitdirPath(config, teamName);
|
|
8436
|
+
await assertWorktreeUsable(worktree);
|
|
8437
|
+
if (await exists(gitdir)) {
|
|
8438
|
+
throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
|
|
8439
|
+
}
|
|
8440
|
+
await ensureDirectory((0, import_node_path6.dirname)(worktree), dryRun);
|
|
8441
|
+
await ensureDirectory((0, import_node_path6.dirname)(gitdir), dryRun);
|
|
8442
|
+
const git = await requiredExecutable("git");
|
|
8443
|
+
await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
|
|
8444
|
+
const newConfig = {
|
|
8445
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8446
|
+
gitdir,
|
|
8447
|
+
name: teamName,
|
|
8448
|
+
remote: remoteUrl,
|
|
8449
|
+
worktree
|
|
8450
|
+
};
|
|
8451
|
+
const updatedTeams = {
|
|
8452
|
+
defaultTeam: shouldSetDefault(options, teamsFile) ? teamName : teamsFile.defaultTeam ?? teamName,
|
|
8453
|
+
teams: { ...teamsFile.teams, [teamName]: newConfig },
|
|
8454
|
+
version: TEAMS_FILE_VERSION
|
|
8455
|
+
};
|
|
8456
|
+
if (dryRun) {
|
|
8457
|
+
console.log(`Would write teams file: ${teamsFilePath(config)}`);
|
|
8458
|
+
console.log(`Would set ${teamName} as default? ${updatedTeams.defaultTeam === teamName}`);
|
|
8459
|
+
} else {
|
|
8460
|
+
await writeTeamsFile(config, updatedTeams);
|
|
8461
|
+
console.log(`Configured shared team "${teamName}" -> ${portablePath(worktree)}`);
|
|
8462
|
+
}
|
|
8463
|
+
if (!dryRun) {
|
|
8464
|
+
await ensureSharedGitignore(worktree, git, options.push !== false);
|
|
8465
|
+
const ingested = await ingestWorktreeFiles(config, newConfig, "create");
|
|
8466
|
+
console.log(`Ingested ${ingested} shared memory file(s) into OpenViking.`);
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
8469
|
+
var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
|
|
8470
|
+
var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
|
|
8471
|
+
async function ensureSharedGitignore(worktree, git, push) {
|
|
8472
|
+
const gitignorePath = (0, import_node_path6.join)(worktree, ".gitignore");
|
|
8473
|
+
const existing = await readFileIfExists(gitignorePath) ?? "";
|
|
8474
|
+
const lines = existing.split("\n").map((line) => line.trim());
|
|
8475
|
+
const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
|
|
8476
|
+
if (missingPatterns.length === 0) {
|
|
8477
|
+
return;
|
|
8478
|
+
}
|
|
8479
|
+
const hasHeader = lines.includes(SHARED_GITIGNORE_HEADER);
|
|
8480
|
+
const segments = [];
|
|
8481
|
+
if (existing.length > 0 && !existing.endsWith("\n")) {
|
|
8482
|
+
segments.push("\n");
|
|
8483
|
+
}
|
|
8484
|
+
if (existing.length > 0) {
|
|
8485
|
+
segments.push("\n");
|
|
8486
|
+
}
|
|
8487
|
+
if (!hasHeader) {
|
|
8488
|
+
segments.push(SHARED_GITIGNORE_HEADER, "\n");
|
|
8489
|
+
}
|
|
8490
|
+
segments.push(missingPatterns.join("\n"), "\n");
|
|
8491
|
+
await (0, import_promises6.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
|
|
8492
|
+
console.log(`Added ${missingPatterns.join(", ")} to ${portablePath(gitignorePath)}`);
|
|
8493
|
+
await maybeRun(false, git, ["-C", worktree, "add", ".gitignore"]);
|
|
8494
|
+
const commitResult = await runCommand(
|
|
8495
|
+
git,
|
|
8496
|
+
["-C", worktree, "commit", "-m", "share: ignore OpenViking directory summaries"],
|
|
8497
|
+
{ allowFailure: true }
|
|
8498
|
+
);
|
|
8499
|
+
if (commitResult.exitCode !== 0) {
|
|
8500
|
+
const detail = commitResult.stderr.trim() || commitResult.stdout.trim();
|
|
8501
|
+
if (!/nothing to commit|no changes added/i.test(detail)) {
|
|
8502
|
+
console.warn(
|
|
8503
|
+
`.gitignore housekeeping commit was rejected (${detail || "unknown"}); it will be retried on the next share sync.`
|
|
8504
|
+
);
|
|
8505
|
+
return;
|
|
8506
|
+
}
|
|
8507
|
+
}
|
|
8508
|
+
if (push) {
|
|
8509
|
+
await maybeRun(false, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8510
|
+
}
|
|
8511
|
+
}
|
|
8512
|
+
async function runShareStatus(config, options) {
|
|
8513
|
+
const team = await resolveTeam(config, options.team);
|
|
8514
|
+
const git = await requiredExecutable("git");
|
|
8515
|
+
console.log(`Team: ${team.name}`);
|
|
8516
|
+
console.log(`Remote: ${team.config.remote}`);
|
|
8517
|
+
console.log(`Worktree: ${portablePath(team.config.worktree)}`);
|
|
8518
|
+
console.log(`Gitdir: ${portablePath(team.config.gitdir)}`);
|
|
8519
|
+
await maybeRun(options.dryRun === true, git, ["-C", team.config.worktree, "status", "--short", "--branch"]);
|
|
8520
|
+
await maybeRun(options.dryRun === true, git, ["-C", team.config.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME], {
|
|
8521
|
+
allowFailure: true
|
|
8522
|
+
});
|
|
8523
|
+
const ahead = await gitOutput(team.config.worktree, ["rev-list", "--count", "@{u}..HEAD"], options.dryRun === true);
|
|
8524
|
+
const behind = await gitOutput(team.config.worktree, ["rev-list", "--count", "HEAD..@{u}"], options.dryRun === true);
|
|
8525
|
+
if (ahead !== void 0) {
|
|
8526
|
+
console.log(`Ahead of upstream: ${ahead}`);
|
|
8527
|
+
}
|
|
8528
|
+
if (behind !== void 0) {
|
|
8529
|
+
console.log(`Behind upstream: ${behind}`);
|
|
8530
|
+
}
|
|
8531
|
+
}
|
|
8532
|
+
async function runShareSync(config, options) {
|
|
8533
|
+
const team = await resolveTeam(config, options.team);
|
|
8534
|
+
const dryRun = options.dryRun === true;
|
|
8535
|
+
const git = await requiredExecutable("git");
|
|
8536
|
+
const worktree = team.config.worktree;
|
|
8537
|
+
if (!dryRun) {
|
|
8538
|
+
await ensureSharedGitignore(worktree, git, false);
|
|
8539
|
+
}
|
|
8540
|
+
if (await hasUncommittedChanges(worktree)) {
|
|
8541
|
+
if (options.autoCommit === false) {
|
|
8542
|
+
throw new Error(
|
|
8543
|
+
`Worktree ${worktree} has uncommitted changes. Commit them yourself or rerun without --no-auto-commit.`
|
|
8544
|
+
);
|
|
8545
|
+
}
|
|
8546
|
+
const message = options.message ?? `share: sync ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
8547
|
+
await stageShareableChanges(dryRun, git, worktree);
|
|
8548
|
+
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
8549
|
+
}
|
|
8550
|
+
const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], dryRun);
|
|
8551
|
+
await maybeRun(dryRun, git, ["-C", worktree, "fetch", DEFAULT_GIT_REMOTE_NAME]);
|
|
8552
|
+
const pullResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8553
|
+
if (dryRun) {
|
|
8554
|
+
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
8555
|
+
} else if (pullResult && pullResult.exitCode !== 0) {
|
|
8556
|
+
if (await exists((0, import_node_path6.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path6.join)(team.config.gitdir, "rebase-apply"))) {
|
|
8557
|
+
throw new Error(
|
|
8558
|
+
`git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
|
|
8559
|
+
Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
|
|
8560
|
+
);
|
|
8561
|
+
}
|
|
8562
|
+
throw new Error(
|
|
8563
|
+
`git pull --rebase failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
|
|
8564
|
+
);
|
|
8565
|
+
}
|
|
8566
|
+
const afterRev = await gitOutput(worktree, ["rev-parse", "HEAD"], dryRun);
|
|
8567
|
+
if (!dryRun && beforeRev && afterRev && beforeRev !== afterRev) {
|
|
8568
|
+
const changes = await listChangedFiles(worktree, beforeRev, afterRev);
|
|
8569
|
+
await applyChangesToOpenViking(config, team.config, changes);
|
|
8570
|
+
console.log(`Reindexed ${changes.length} file change(s) into OpenViking.`);
|
|
8571
|
+
} else if (!dryRun) {
|
|
8572
|
+
console.log("No upstream changes to reindex.");
|
|
8573
|
+
}
|
|
8574
|
+
if (options.push !== false) {
|
|
8575
|
+
await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8576
|
+
}
|
|
8577
|
+
}
|
|
8578
|
+
async function stageShareableChanges(dryRun, git, worktree) {
|
|
8579
|
+
const pathspecs = [":(top)README.md", ":(top).gitignore", ...SHAREABLE_MEMORY_KIND_DIRS.map((dir) => `:(top)${dir}`)];
|
|
8580
|
+
await maybeRun(dryRun, git, ["-C", worktree, "add", "--", ...pathspecs], { allowFailure: true });
|
|
8581
|
+
}
|
|
8582
|
+
async function runSharePublish(config, sourceUri, options) {
|
|
8583
|
+
assertVikingUri(sourceUri);
|
|
8584
|
+
const team = await resolveTeam(config, options.team);
|
|
8585
|
+
const dryRun = options.dryRun === true;
|
|
8586
|
+
if (isInSharedNamespace(config, sourceUri)) {
|
|
8587
|
+
throw new Error(`Memory ${sourceUri} is already in the shared namespace.`);
|
|
8588
|
+
}
|
|
8589
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
8590
|
+
const content = await readMemoryContent(config, ov, sourceUri, dryRun);
|
|
8591
|
+
const blocker = scrubberBlocker(content);
|
|
8592
|
+
if (blocker) {
|
|
8593
|
+
throw new Error(`Refusing to publish ${sourceUri}: possible ${blocker}. Strip the sensitive value, then retry.`);
|
|
8594
|
+
}
|
|
8595
|
+
const targetUri = sharedUriFor(config, sourceUri, team.name);
|
|
8596
|
+
if (!dryRun && await vikingResourceExists2(ov, config, targetUri)) {
|
|
8597
|
+
throw new Error(
|
|
8598
|
+
`Refusing to publish: ${targetUri} already exists in the shared namespace. Inspect it via threadnote read; if it should be replaced, forget the existing shared copy first.`
|
|
8599
|
+
);
|
|
8600
|
+
}
|
|
8601
|
+
await ensureSharedDirectoryChain(config, ov, targetUri, dryRun);
|
|
8602
|
+
await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
|
|
8603
|
+
await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "publish");
|
|
8604
|
+
const git = await requiredExecutable("git");
|
|
8605
|
+
const worktree = team.config.worktree;
|
|
8606
|
+
const relativePath = vikingUriToWorktreeRelative(config, targetUri, team.name);
|
|
8607
|
+
const message = options.message ?? `share: publish ${relativePath}`;
|
|
8608
|
+
await maybeRun(dryRun, git, ["-C", worktree, "add", "--", relativePath]);
|
|
8609
|
+
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
8610
|
+
if (options.push !== false) {
|
|
8611
|
+
const pushResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8612
|
+
if (dryRun) {
|
|
8613
|
+
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
8614
|
+
} else if (pushResult && pushResult.exitCode !== 0) {
|
|
8615
|
+
const detail = pushResult.stderr.trim() || pushResult.stdout.trim() || "unknown error";
|
|
8616
|
+
throw new Error(
|
|
8617
|
+
`Memory was committed locally but git push failed: ${detail}
|
|
8618
|
+
Resolve the remote issue (auth, network, branch protection), then run: threadnote share sync`
|
|
8619
|
+
);
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
8622
|
+
console.log(`Published ${sourceUri} -> ${targetUri}`);
|
|
8623
|
+
}
|
|
8624
|
+
async function runShareUnpublish(config, sourceUri, options) {
|
|
8625
|
+
assertVikingUri(sourceUri);
|
|
8626
|
+
const team = await resolveTeam(config, options.team);
|
|
8627
|
+
const dryRun = options.dryRun === true;
|
|
8628
|
+
if (!isInTeamNamespace(config, sourceUri, team.name)) {
|
|
8629
|
+
throw new Error(`Memory ${sourceUri} is not in team "${team.name}" shared namespace.`);
|
|
8630
|
+
}
|
|
8631
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
8632
|
+
const content = await readMemoryContent(config, ov, sourceUri, dryRun);
|
|
8633
|
+
const targetUri = personalUriFor(config, sourceUri, team.name);
|
|
8634
|
+
if (!dryRun && await vikingResourceExists2(ov, config, targetUri)) {
|
|
8635
|
+
throw new Error(
|
|
8636
|
+
`Refusing to unpublish: a personal memory already exists at ${targetUri}. Move or forget it first, then retry.`
|
|
8637
|
+
);
|
|
8638
|
+
}
|
|
8639
|
+
await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
|
|
8640
|
+
await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "unpublish");
|
|
8641
|
+
const git = await requiredExecutable("git");
|
|
8642
|
+
const worktree = team.config.worktree;
|
|
8643
|
+
const relativePath = vikingUriToWorktreeRelative(config, sourceUri, team.name);
|
|
8644
|
+
const message = options.message ?? `share: unpublish ${relativePath}`;
|
|
8645
|
+
await maybeRun(dryRun, git, ["-C", worktree, "rm", relativePath], { allowFailure: true });
|
|
8646
|
+
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
8647
|
+
if (options.push !== false) {
|
|
8648
|
+
await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8649
|
+
}
|
|
8650
|
+
console.log(`Unpublished ${sourceUri} -> ${targetUri}`);
|
|
8651
|
+
}
|
|
8652
|
+
async function runShareList(config, _options) {
|
|
8653
|
+
const teams = await readTeamsFile(config);
|
|
8654
|
+
const entries = Object.values(teams.teams);
|
|
8655
|
+
if (entries.length === 0) {
|
|
8656
|
+
console.log("No shared teams configured. Run: threadnote share init <remote-url>");
|
|
8657
|
+
return;
|
|
8658
|
+
}
|
|
8659
|
+
for (const team of entries) {
|
|
8660
|
+
const marker = team.name === teams.defaultTeam ? " (default)" : "";
|
|
8661
|
+
console.log(`- ${team.name}${marker}`);
|
|
8662
|
+
console.log(` remote: ${team.remote}`);
|
|
8663
|
+
console.log(` worktree: ${portablePath(team.worktree)}`);
|
|
8664
|
+
console.log(` gitdir: ${portablePath(team.gitdir)}`);
|
|
8665
|
+
console.log(` added: ${team.addedAt}`);
|
|
8666
|
+
}
|
|
8667
|
+
}
|
|
8668
|
+
async function runShareRemove(config, options) {
|
|
8669
|
+
const team = await resolveTeam(config, options.team);
|
|
8670
|
+
const dryRun = options.dryRun === true;
|
|
8671
|
+
const teamsFile = await readTeamsFile(config);
|
|
8672
|
+
const remaining = {};
|
|
8673
|
+
for (const [name, value] of Object.entries(teamsFile.teams)) {
|
|
8674
|
+
if (name !== team.name) {
|
|
8675
|
+
remaining[name] = value;
|
|
8676
|
+
}
|
|
8677
|
+
}
|
|
8678
|
+
const remainingNames = Object.keys(remaining);
|
|
8679
|
+
const nextDefault = teamsFile.defaultTeam === team.name ? remainingNames[0] : teamsFile.defaultTeam;
|
|
8680
|
+
const updated = { defaultTeam: nextDefault, teams: remaining, version: TEAMS_FILE_VERSION };
|
|
8681
|
+
if (dryRun) {
|
|
8682
|
+
console.log(`Would update teams file: ${teamsFilePath(config)}`);
|
|
8683
|
+
} else {
|
|
8684
|
+
await writeTeamsFile(config, updated);
|
|
8685
|
+
console.log(`Removed team "${team.name}" from teams.json.`);
|
|
8686
|
+
}
|
|
8687
|
+
if (options.keepFiles !== true) {
|
|
8688
|
+
await removePath(team.config.worktree, "shared worktree", dryRun);
|
|
8689
|
+
await removePath(team.config.gitdir, "shared gitdir", dryRun);
|
|
8690
|
+
} else {
|
|
8691
|
+
console.log(`Keeping files at ${portablePath(team.config.worktree)} and ${portablePath(team.config.gitdir)}`);
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8694
|
+
function normalizeTeamName(input2) {
|
|
8695
|
+
const candidate = (input2 ?? "default").trim();
|
|
8696
|
+
if (!candidate) {
|
|
8697
|
+
return "default";
|
|
8698
|
+
}
|
|
8699
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(candidate) || /^\.+$/.test(candidate)) {
|
|
8700
|
+
throw new Error(
|
|
8701
|
+
`Invalid team name "${input2}". Team names must start with a lowercase letter or digit and contain only [a-z0-9._-]. Single-dot or dot-only names are rejected so they don't collapse to the shared-root or parent directory.`
|
|
8702
|
+
);
|
|
8703
|
+
}
|
|
8704
|
+
return candidate;
|
|
8705
|
+
}
|
|
8706
|
+
function teamsFilePath(config) {
|
|
8707
|
+
return (0, import_node_path6.join)(config.agentContextHome, "share", "teams.json");
|
|
8708
|
+
}
|
|
8709
|
+
function teamWorktreePath(config, team) {
|
|
8710
|
+
return (0, import_node_path6.join)(
|
|
8711
|
+
config.agentContextHome,
|
|
8712
|
+
"data",
|
|
8713
|
+
"viking",
|
|
8714
|
+
config.account,
|
|
8715
|
+
"user",
|
|
8716
|
+
uriSegment(config.user),
|
|
8717
|
+
"memories",
|
|
8718
|
+
SHARED_SEGMENT,
|
|
8719
|
+
team
|
|
8720
|
+
);
|
|
8721
|
+
}
|
|
8722
|
+
function teamGitdirPath(config, team) {
|
|
8723
|
+
return (0, import_node_path6.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
|
|
8724
|
+
}
|
|
8725
|
+
async function readTeamsFile(config) {
|
|
8726
|
+
const path = teamsFilePath(config);
|
|
8727
|
+
const raw = await readFileIfExists(path);
|
|
8728
|
+
if (!raw) {
|
|
8729
|
+
return { teams: {}, version: TEAMS_FILE_VERSION };
|
|
8730
|
+
}
|
|
8731
|
+
const parsed = parseJsonConfigObject(raw);
|
|
8732
|
+
if (!parsed) {
|
|
8733
|
+
throw new Error(`Could not parse teams file ${path}`);
|
|
8734
|
+
}
|
|
8735
|
+
if (typeof parsed.version === "number" && parsed.version > TEAMS_FILE_VERSION) {
|
|
8736
|
+
throw new Error(
|
|
8737
|
+
`Teams file ${path} was written with version ${parsed.version}; this Threadnote binary understands up to version ${TEAMS_FILE_VERSION}. Upgrade Threadnote (\`threadnote update\`) before continuing.`
|
|
8738
|
+
);
|
|
8739
|
+
}
|
|
8740
|
+
const teams = {};
|
|
8741
|
+
if (typeof parsed.teams === "object" && parsed.teams !== null && !Array.isArray(parsed.teams)) {
|
|
8742
|
+
for (const [name, value] of Object.entries(parsed.teams)) {
|
|
8743
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
8744
|
+
console.warn(`Skipping non-object team entry "${name}" in ${path}.`);
|
|
8745
|
+
continue;
|
|
8746
|
+
}
|
|
8747
|
+
const entry = value;
|
|
8748
|
+
if (typeof entry.remote !== "string" || entry.remote.length === 0) {
|
|
8749
|
+
console.warn(`Skipping team entry "${name}" in ${path}: missing or empty "remote" field.`);
|
|
8750
|
+
continue;
|
|
8751
|
+
}
|
|
8752
|
+
teams[name] = {
|
|
8753
|
+
addedAt: typeof entry.addedAt === "string" ? entry.addedAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
8754
|
+
gitdir: typeof entry.gitdir === "string" ? entry.gitdir : teamGitdirPath(config, name),
|
|
8755
|
+
name,
|
|
8756
|
+
remote: entry.remote,
|
|
8757
|
+
worktree: typeof entry.worktree === "string" ? entry.worktree : teamWorktreePath(config, name)
|
|
8758
|
+
};
|
|
8759
|
+
}
|
|
8760
|
+
}
|
|
8761
|
+
const defaultTeam = typeof parsed.defaultTeam === "string" ? parsed.defaultTeam : void 0;
|
|
8762
|
+
return { defaultTeam, teams, version: TEAMS_FILE_VERSION };
|
|
8763
|
+
}
|
|
8764
|
+
async function writeTeamsFile(config, contents) {
|
|
8765
|
+
const path = teamsFilePath(config);
|
|
8766
|
+
await (0, import_promises6.mkdir)((0, import_node_path6.dirname)(path), { recursive: true });
|
|
8767
|
+
const serializable = {
|
|
8768
|
+
defaultTeam: contents.defaultTeam,
|
|
8769
|
+
teams: contents.teams,
|
|
8770
|
+
version: contents.version
|
|
8771
|
+
};
|
|
8772
|
+
await (0, import_promises6.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
|
|
8773
|
+
`, { encoding: "utf8", mode: 384 });
|
|
8774
|
+
}
|
|
8775
|
+
async function resolveTeam(config, requested) {
|
|
8776
|
+
const teamsFile = await readTeamsFile(config);
|
|
8777
|
+
const entries = Object.entries(teamsFile.teams);
|
|
8778
|
+
if (entries.length === 0) {
|
|
8779
|
+
throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
|
|
8780
|
+
}
|
|
8781
|
+
const wantName = requested ? normalizeTeamName(requested) : teamsFile.defaultTeam ?? entries[0][0];
|
|
8782
|
+
const found = teamsFile.teams[wantName];
|
|
8783
|
+
if (!found) {
|
|
8784
|
+
const known = entries.map(([name]) => name).join(", ");
|
|
8785
|
+
throw new Error(`Team "${wantName}" is not configured. Known teams: ${known}`);
|
|
8786
|
+
}
|
|
8787
|
+
return { config: found, name: wantName };
|
|
8788
|
+
}
|
|
8789
|
+
function shouldSetDefault(options, existing) {
|
|
8790
|
+
if (options.setDefault === true) {
|
|
8791
|
+
return true;
|
|
8792
|
+
}
|
|
8793
|
+
return existing.defaultTeam === void 0;
|
|
8794
|
+
}
|
|
8795
|
+
async function assertWorktreeUsable(worktree) {
|
|
8796
|
+
if (!await exists(worktree)) {
|
|
8797
|
+
return;
|
|
8798
|
+
}
|
|
8799
|
+
if (!await isDirectory(worktree)) {
|
|
8800
|
+
throw new Error(`Cannot use ${worktree} as a worktree: not a directory.`);
|
|
8801
|
+
}
|
|
8802
|
+
const entries = await (0, import_promises6.readdir)(worktree);
|
|
8803
|
+
if (entries.length > 0) {
|
|
8804
|
+
const preview = entries.slice(0, 5).join(", ");
|
|
8805
|
+
const suffix = entries.length > 5 ? `, +${entries.length - 5} more` : "";
|
|
8806
|
+
throw new Error(
|
|
8807
|
+
`Worktree ${worktree} is not empty (contains: ${preview}${suffix}). Move or remove its contents, then retry threadnote share init.`
|
|
8808
|
+
);
|
|
8809
|
+
}
|
|
8810
|
+
}
|
|
8811
|
+
async function ingestWorktreeFiles(config, team, initialMode) {
|
|
8812
|
+
const ov = await openVikingCliForMode(false);
|
|
8813
|
+
const files = await walkMemoryFiles(team.worktree);
|
|
8814
|
+
for (const file of files) {
|
|
8815
|
+
const uri = workfileToVikingUri(config, team, file);
|
|
8816
|
+
await ensureSharedDirectoryChain(config, ov, uri, false);
|
|
8817
|
+
await ingestSingleFile(ov, config, uri, file, initialMode);
|
|
8818
|
+
}
|
|
8819
|
+
return files.length;
|
|
8820
|
+
}
|
|
8821
|
+
async function walkMemoryFiles(root) {
|
|
8822
|
+
const out = [];
|
|
8823
|
+
async function visit(path, depth) {
|
|
8824
|
+
let entries;
|
|
8825
|
+
try {
|
|
8826
|
+
entries = await (0, import_promises6.readdir)(path, { withFileTypes: true });
|
|
8827
|
+
} catch (err) {
|
|
8828
|
+
console.warn(`Skipping ${path} during shared-tree walk: ${err instanceof Error ? err.message : String(err)}`);
|
|
8829
|
+
return;
|
|
8830
|
+
}
|
|
8831
|
+
for (const entry of entries) {
|
|
8832
|
+
if (entry.name === ".git") {
|
|
8833
|
+
continue;
|
|
8834
|
+
}
|
|
8835
|
+
const full = (0, import_node_path6.join)(path, entry.name);
|
|
8836
|
+
if (entry.isDirectory()) {
|
|
8837
|
+
if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
|
|
8838
|
+
continue;
|
|
8839
|
+
}
|
|
8840
|
+
await visit(full, depth + 1);
|
|
8841
|
+
continue;
|
|
8842
|
+
}
|
|
8843
|
+
if (!entry.isFile()) {
|
|
8844
|
+
continue;
|
|
8845
|
+
}
|
|
8846
|
+
if (depth === 0) {
|
|
8847
|
+
continue;
|
|
8848
|
+
}
|
|
8849
|
+
if (!entry.name.endsWith(".md")) {
|
|
8850
|
+
continue;
|
|
8851
|
+
}
|
|
8852
|
+
out.push(full);
|
|
8853
|
+
}
|
|
8854
|
+
}
|
|
8855
|
+
await visit(root, 0);
|
|
8856
|
+
return out;
|
|
8857
|
+
}
|
|
8858
|
+
function workfileToVikingUri(config, team, filePath) {
|
|
8859
|
+
const rel = (0, import_node_path6.relative)(team.worktree, filePath).split(import_node_path6.sep).join("/");
|
|
8860
|
+
return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
8861
|
+
}
|
|
8862
|
+
function isInSharedNamespace(config, uri) {
|
|
8863
|
+
return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`);
|
|
8864
|
+
}
|
|
8865
|
+
function isInTeamNamespace(config, uri, team) {
|
|
8866
|
+
return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`);
|
|
8867
|
+
}
|
|
8868
|
+
function sharedUriFor(config, personalUri, team) {
|
|
8869
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/`;
|
|
8870
|
+
if (!personalUri.startsWith(prefix)) {
|
|
8871
|
+
throw new Error(`Refusing to publish memory outside the current user namespace: ${personalUri}`);
|
|
8872
|
+
}
|
|
8873
|
+
const rest = personalUri.slice(prefix.length);
|
|
8874
|
+
return `${prefix}${SHARED_SEGMENT}/${team}/${rest}`;
|
|
8875
|
+
}
|
|
8876
|
+
function personalUriFor(config, sharedUri, team) {
|
|
8877
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`;
|
|
8878
|
+
if (!sharedUri.startsWith(prefix)) {
|
|
8879
|
+
throw new Error(`Refusing to unpublish a URI outside team "${team}" shared namespace: ${sharedUri}`);
|
|
8880
|
+
}
|
|
8881
|
+
const rest = sharedUri.slice(prefix.length);
|
|
8882
|
+
return `viking://user/${uriSegment(config.user)}/memories/${rest}`;
|
|
8883
|
+
}
|
|
8884
|
+
function vikingUriToWorktreeRelative(config, uri, team) {
|
|
8885
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`;
|
|
8886
|
+
if (!uri.startsWith(prefix)) {
|
|
8887
|
+
throw new Error(`URI ${uri} is not inside team "${team}" shared subtree.`);
|
|
8888
|
+
}
|
|
8889
|
+
return uri.slice(prefix.length);
|
|
8890
|
+
}
|
|
8891
|
+
function scrubberBlocker(content) {
|
|
8892
|
+
return SCRUBBER_PATTERNS.find((pattern) => pattern.regex.test(content))?.name;
|
|
8893
|
+
}
|
|
8894
|
+
async function readMemoryContent(config, ov, uri, dryRun) {
|
|
8895
|
+
const args = withIdentity(config, ["read", uri]);
|
|
8896
|
+
if (dryRun) {
|
|
8897
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8898
|
+
return "<dry-run memory body>";
|
|
8899
|
+
}
|
|
8900
|
+
const result = await runCommand(ov, args);
|
|
8901
|
+
if (!result.stdout.trim()) {
|
|
8902
|
+
throw new Error(`Refusing to publish empty memory at ${uri}`);
|
|
8903
|
+
}
|
|
8904
|
+
return result.stdout;
|
|
8905
|
+
}
|
|
8906
|
+
async function ensureSharedDirectoryChain(config, ov, memoryUri, dryRun) {
|
|
8907
|
+
const directoryUri = parentUri(memoryUri);
|
|
8908
|
+
for (const uri of sharedDirectoryChain(config, directoryUri)) {
|
|
8909
|
+
const args = withIdentity(config, ["stat", uri]);
|
|
8910
|
+
if (dryRun) {
|
|
8911
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8912
|
+
continue;
|
|
8913
|
+
}
|
|
8914
|
+
const statResult = await runCommand(ov, args, { allowFailure: true });
|
|
8915
|
+
if (statResult.exitCode === 0) {
|
|
8916
|
+
continue;
|
|
8917
|
+
}
|
|
8918
|
+
await maybeRun(false, ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared memories."]));
|
|
8919
|
+
}
|
|
8920
|
+
}
|
|
8921
|
+
function parentUri(uri) {
|
|
8922
|
+
const lastSlash = uri.lastIndexOf("/");
|
|
8923
|
+
return lastSlash === -1 ? uri : uri.slice(0, lastSlash);
|
|
8924
|
+
}
|
|
8925
|
+
function sharedDirectoryChain(config, directoryUri) {
|
|
8926
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
|
|
8927
|
+
if (!directoryUri.startsWith(prefix)) {
|
|
8928
|
+
return [directoryUri];
|
|
8929
|
+
}
|
|
8930
|
+
const parts = directoryUri.slice(prefix.length).split("/").filter(Boolean);
|
|
8931
|
+
const chain = [];
|
|
8932
|
+
for (let index = 1; index <= parts.length; index += 1) {
|
|
8933
|
+
chain.push(`${prefix}${parts.slice(0, index).join("/")}`);
|
|
8934
|
+
}
|
|
8935
|
+
return chain;
|
|
8936
|
+
}
|
|
8937
|
+
async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun) {
|
|
8938
|
+
if (dryRun) {
|
|
8939
|
+
const args = withIdentity(config, [
|
|
8940
|
+
"write",
|
|
8941
|
+
uri,
|
|
8942
|
+
"--from-file",
|
|
8943
|
+
"<staged temp file>",
|
|
8944
|
+
"--mode",
|
|
8945
|
+
initialMode,
|
|
8946
|
+
"--wait",
|
|
8947
|
+
"--timeout",
|
|
8948
|
+
"120"
|
|
8949
|
+
]);
|
|
8950
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8951
|
+
return;
|
|
8952
|
+
}
|
|
8953
|
+
const stagingDir = await (0, import_promises6.mkdtemp)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
|
|
8954
|
+
const tempPath = (0, import_node_path6.join)(stagingDir, "body.txt");
|
|
8955
|
+
try {
|
|
8956
|
+
await (0, import_promises6.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
|
|
8957
|
+
await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode);
|
|
8958
|
+
} finally {
|
|
8959
|
+
await (0, import_promises6.rm)(stagingDir, { force: true, recursive: true });
|
|
8960
|
+
}
|
|
8961
|
+
}
|
|
8962
|
+
async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode) {
|
|
8963
|
+
const maxAttempts = 4;
|
|
8964
|
+
const existedBeforeWrite = await vikingResourceExists2(ov, config, uri);
|
|
8965
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
8966
|
+
const existsNow = attempt === 0 ? existedBeforeWrite : await vikingResourceExists2(ov, config, uri);
|
|
8967
|
+
const ourWriteLanded = existsNow && !existedBeforeWrite;
|
|
8968
|
+
const mode = attempt === 0 ? initialMode : ourWriteLanded ? "replace" : initialMode;
|
|
8969
|
+
const args = withIdentity(config, [
|
|
8970
|
+
"write",
|
|
8971
|
+
uri,
|
|
8972
|
+
"--from-file",
|
|
8973
|
+
fromFile,
|
|
8974
|
+
"--mode",
|
|
8975
|
+
mode,
|
|
8976
|
+
"--wait",
|
|
8977
|
+
"--timeout",
|
|
8978
|
+
"120"
|
|
8979
|
+
]);
|
|
8980
|
+
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
8981
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
8982
|
+
if (result.exitCode === 0) {
|
|
8983
|
+
if (result.stdout.trim()) {
|
|
8984
|
+
console.log(result.stdout.trim());
|
|
8985
|
+
}
|
|
8986
|
+
if (result.stderr.trim()) {
|
|
8987
|
+
console.error(result.stderr.trim());
|
|
8988
|
+
}
|
|
8989
|
+
return;
|
|
8990
|
+
}
|
|
8991
|
+
if (isTransientOvFailure(result.stderr, result.stdout) && await vikingResourceExists2(ov, config, uri) && !existedBeforeWrite) {
|
|
8992
|
+
console.log("OpenViking accepted the write but returned an error before the wait completed; draining the queue.");
|
|
8993
|
+
await waitForOvQueue(ov, config);
|
|
8994
|
+
return;
|
|
8995
|
+
}
|
|
8996
|
+
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
8997
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
8998
|
+
}
|
|
8999
|
+
await sleep(1e3 * (attempt + 1));
|
|
9000
|
+
}
|
|
9001
|
+
}
|
|
9002
|
+
async function waitForOvQueue(ov, config) {
|
|
9003
|
+
const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
|
|
9004
|
+
if (result.stdout.trim()) {
|
|
9005
|
+
console.log(result.stdout.trim());
|
|
9006
|
+
}
|
|
9007
|
+
if (result.stderr.trim()) {
|
|
9008
|
+
console.error(result.stderr.trim());
|
|
9009
|
+
}
|
|
9010
|
+
}
|
|
9011
|
+
function isTransientOvFailure(stderr, stdout) {
|
|
9012
|
+
const output2 = `${stderr}
|
|
9013
|
+
${stdout}`.toLowerCase();
|
|
9014
|
+
return output2.includes("resource is busy") || output2.includes("resource is being processed") || output2.includes("network error") || output2.includes("error sending request") || output2.includes("http request failed") || output2.includes("connection refused") || output2.includes("connection reset") || output2.includes("timed out");
|
|
9015
|
+
}
|
|
9016
|
+
async function ingestSingleFile(ov, config, uri, filePath, initialMode) {
|
|
9017
|
+
const content = await (0, import_promises6.readFile)(filePath, "utf8");
|
|
9018
|
+
await writeMemoryFile(config, ov, uri, content, initialMode, false);
|
|
9019
|
+
}
|
|
9020
|
+
async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
|
|
9021
|
+
try {
|
|
9022
|
+
await removeMemoryUri(config, ov, sourceUri, dryRun);
|
|
9023
|
+
} catch (sourceErr) {
|
|
9024
|
+
if (dryRun) {
|
|
9025
|
+
throw sourceErr;
|
|
9026
|
+
}
|
|
9027
|
+
console.error(
|
|
9028
|
+
`Source removal failed during ${label}; rolling back ${rollbackUri} so the system is back to the pre-${label} state.`
|
|
9029
|
+
);
|
|
9030
|
+
try {
|
|
9031
|
+
await removeMemoryUri(config, ov, rollbackUri, false);
|
|
9032
|
+
} catch (rollbackErr) {
|
|
9033
|
+
console.error(
|
|
9034
|
+
`Rollback of ${rollbackUri} also failed. Manual cleanup needed via: threadnote forget ${rollbackUri}
|
|
9035
|
+
Rollback error: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`
|
|
9036
|
+
);
|
|
9037
|
+
}
|
|
9038
|
+
await bestEffortRemoveWorktreeFile(rollbackUri, worktree, label);
|
|
9039
|
+
throw sourceErr;
|
|
9040
|
+
}
|
|
9041
|
+
}
|
|
9042
|
+
async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
9043
|
+
if (label !== "publish") {
|
|
9044
|
+
return;
|
|
9045
|
+
}
|
|
9046
|
+
const prefix = "viking://";
|
|
9047
|
+
if (!rollbackUri.startsWith(prefix)) {
|
|
9048
|
+
return;
|
|
9049
|
+
}
|
|
9050
|
+
const parts = rollbackUri.slice(prefix.length).split("/");
|
|
9051
|
+
const sharedIndex = parts.indexOf("shared");
|
|
9052
|
+
if (sharedIndex === -1 || sharedIndex + 2 >= parts.length) {
|
|
9053
|
+
return;
|
|
9054
|
+
}
|
|
9055
|
+
const relative3 = parts.slice(sharedIndex + 2).join("/");
|
|
9056
|
+
if (!relative3) {
|
|
9057
|
+
return;
|
|
9058
|
+
}
|
|
9059
|
+
await (0, import_promises6.rm)((0, import_node_path6.join)(worktree, relative3), { force: true });
|
|
9060
|
+
}
|
|
9061
|
+
async function removeMemoryUri(config, ov, uri, dryRun) {
|
|
9062
|
+
const args = withIdentity(config, ["rm", uri]);
|
|
9063
|
+
if (dryRun) {
|
|
9064
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
9065
|
+
return;
|
|
9066
|
+
}
|
|
9067
|
+
const maxAttempts = 4;
|
|
9068
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
9069
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
9070
|
+
if (result.exitCode === 0) {
|
|
9071
|
+
if (result.stdout.trim()) {
|
|
9072
|
+
console.log(result.stdout.trim());
|
|
9073
|
+
}
|
|
9074
|
+
return;
|
|
9075
|
+
}
|
|
9076
|
+
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
9077
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
9078
|
+
}
|
|
9079
|
+
await sleep(1e3 * (attempt + 1));
|
|
9080
|
+
}
|
|
9081
|
+
}
|
|
9082
|
+
async function vikingResourceExists2(ov, config, uri) {
|
|
9083
|
+
const result = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
9084
|
+
return result.exitCode === 0;
|
|
9085
|
+
}
|
|
9086
|
+
async function hasUncommittedChanges(worktree) {
|
|
9087
|
+
const result = await runCommand("git", ["-C", worktree, "status", "--porcelain"], { allowFailure: true });
|
|
9088
|
+
return result.stdout.trim().length > 0;
|
|
9089
|
+
}
|
|
9090
|
+
async function gitOutput(worktree, args, dryRun) {
|
|
9091
|
+
if (dryRun) {
|
|
9092
|
+
return void 0;
|
|
9093
|
+
}
|
|
9094
|
+
const result = await runCommand("git", ["-C", worktree, ...args], { allowFailure: true });
|
|
9095
|
+
if (result.exitCode !== 0) {
|
|
9096
|
+
return void 0;
|
|
9097
|
+
}
|
|
9098
|
+
return result.stdout.trim();
|
|
9099
|
+
}
|
|
9100
|
+
async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
9101
|
+
const result = await runCommand("git", ["-C", worktree, "diff", "--name-status", "-z", `${beforeRev}..${afterRev}`], {
|
|
9102
|
+
allowFailure: true
|
|
9103
|
+
});
|
|
9104
|
+
if (result.exitCode !== 0) {
|
|
9105
|
+
return [];
|
|
9106
|
+
}
|
|
9107
|
+
const entries = result.stdout.split("\0").filter((part) => part.length > 0);
|
|
9108
|
+
const changes = [];
|
|
9109
|
+
for (let index = 0; index < entries.length; ) {
|
|
9110
|
+
const raw = entries[index];
|
|
9111
|
+
const head = raw.slice(0, 1);
|
|
9112
|
+
if (head === "R" || head === "C") {
|
|
9113
|
+
const oldRel = entries[index + 1];
|
|
9114
|
+
const newRel = entries[index + 2];
|
|
9115
|
+
if (oldRel && newRel) {
|
|
9116
|
+
changes.push({ path: (0, import_node_path6.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
9117
|
+
changes.push({ path: (0, import_node_path6.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
9118
|
+
}
|
|
9119
|
+
index += 3;
|
|
9120
|
+
continue;
|
|
9121
|
+
}
|
|
9122
|
+
const rel = entries[index + 1];
|
|
9123
|
+
if (rel) {
|
|
9124
|
+
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
9125
|
+
changes.push({ path: (0, import_node_path6.join)(worktree, rel), relativePath: rel, status });
|
|
9126
|
+
}
|
|
9127
|
+
index += 2;
|
|
9128
|
+
}
|
|
9129
|
+
return changes;
|
|
9130
|
+
}
|
|
9131
|
+
async function applyChangesToOpenViking(config, team, changes) {
|
|
9132
|
+
const ov = await openVikingCliForMode(false);
|
|
9133
|
+
for (const change of changes) {
|
|
9134
|
+
if (!change.relativePath.endsWith(".md")) {
|
|
9135
|
+
continue;
|
|
9136
|
+
}
|
|
9137
|
+
const firstSegment = change.relativePath.split("/")[0];
|
|
9138
|
+
if (!SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment)) {
|
|
9139
|
+
continue;
|
|
9140
|
+
}
|
|
9141
|
+
const uri = workfileToVikingUri(config, team, change.path);
|
|
9142
|
+
if (change.status === "removed") {
|
|
9143
|
+
await removeMemoryUri(config, ov, uri, false);
|
|
9144
|
+
continue;
|
|
9145
|
+
}
|
|
9146
|
+
if (!await isFile(change.path)) {
|
|
9147
|
+
continue;
|
|
9148
|
+
}
|
|
9149
|
+
if (change.status === "modified" && await vikingResourceExists2(ov, config, uri)) {
|
|
9150
|
+
console.warn(
|
|
9151
|
+
`share sync: overwriting local ${uri} with the upstream version (local edits to the shared subtree are not preserved across sync).`
|
|
9152
|
+
);
|
|
9153
|
+
}
|
|
9154
|
+
await ensureSharedDirectoryChain(config, ov, uri, false);
|
|
9155
|
+
const writeMode = change.status === "modified" ? "replace" : "create";
|
|
9156
|
+
await ingestSingleFile(ov, config, uri, change.path, writeMode);
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
9159
|
+
|
|
8398
9160
|
// src/lifecycle.ts
|
|
8399
9161
|
var import_node_child_process2 = require("node:child_process");
|
|
8400
9162
|
var import_node_fs5 = require("node:fs");
|
|
8401
|
-
var
|
|
8402
|
-
var
|
|
8403
|
-
var
|
|
9163
|
+
var import_promises9 = require("node:fs/promises");
|
|
9164
|
+
var import_node_os6 = require("node:os");
|
|
9165
|
+
var import_node_path8 = require("node:path");
|
|
8404
9166
|
|
|
8405
9167
|
// src/update.ts
|
|
8406
9168
|
var import_node_fs4 = require("node:fs");
|
|
8407
|
-
var
|
|
8408
|
-
var
|
|
8409
|
-
var
|
|
8410
|
-
var
|
|
9169
|
+
var import_promises7 = require("node:fs/promises");
|
|
9170
|
+
var import_node_os5 = require("node:os");
|
|
9171
|
+
var import_node_path7 = require("node:path");
|
|
9172
|
+
var import_promises8 = require("node:readline/promises");
|
|
8411
9173
|
var import_node_process = require("node:process");
|
|
8412
9174
|
var NPM_PACKAGE_NAME = "threadnote";
|
|
8413
9175
|
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
@@ -8555,7 +9317,7 @@ async function getUpdateInfo(config, options) {
|
|
|
8555
9317
|
};
|
|
8556
9318
|
}
|
|
8557
9319
|
async function currentPackageVersion() {
|
|
8558
|
-
const rawPackage = await (0,
|
|
9320
|
+
const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
|
|
8559
9321
|
const parsed = JSON.parse(rawPackage);
|
|
8560
9322
|
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8561
9323
|
throw new Error("Could not read current threadnote package version.");
|
|
@@ -8605,11 +9367,11 @@ async function readFreshCache(config, registry) {
|
|
|
8605
9367
|
}
|
|
8606
9368
|
async function writeUpdateCache(config, cache) {
|
|
8607
9369
|
await ensureDirectory(config.agentContextHome, false);
|
|
8608
|
-
await (0,
|
|
9370
|
+
await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
|
|
8609
9371
|
`, { encoding: "utf8", mode: 384 });
|
|
8610
9372
|
}
|
|
8611
9373
|
function updateCachePath(config) {
|
|
8612
|
-
return (0,
|
|
9374
|
+
return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
|
|
8613
9375
|
}
|
|
8614
9376
|
async function runApplicablePostUpdateMigrations(config, options) {
|
|
8615
9377
|
const state = await readPostUpdateState(config);
|
|
@@ -8673,7 +9435,7 @@ async function applicablePostUpdateMigrations(config, options) {
|
|
|
8673
9435
|
return applicable;
|
|
8674
9436
|
}
|
|
8675
9437
|
async function readPostUpdateMigrations() {
|
|
8676
|
-
const raw = await readFileIfExists((0,
|
|
9438
|
+
const raw = await readFileIfExists((0, import_node_path7.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
|
|
8677
9439
|
if (!raw) {
|
|
8678
9440
|
return [];
|
|
8679
9441
|
}
|
|
@@ -8712,7 +9474,7 @@ function printPostUpdateMigration(migration) {
|
|
|
8712
9474
|
}
|
|
8713
9475
|
}
|
|
8714
9476
|
async function confirmPostUpdateMigration(prompt) {
|
|
8715
|
-
const readline = (0,
|
|
9477
|
+
const readline = (0, import_promises8.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
8716
9478
|
try {
|
|
8717
9479
|
const answer = (await readline.question(prompt)).trim().toLowerCase();
|
|
8718
9480
|
return answer === "y" || answer === "yes";
|
|
@@ -8744,11 +9506,11 @@ async function readPostUpdateState(config) {
|
|
|
8744
9506
|
}
|
|
8745
9507
|
async function writePostUpdateState(config, state) {
|
|
8746
9508
|
await ensureDirectory(config.agentContextHome, false);
|
|
8747
|
-
await (0,
|
|
9509
|
+
await (0, import_promises7.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
|
|
8748
9510
|
`, { encoding: "utf8", mode: 384 });
|
|
8749
9511
|
}
|
|
8750
9512
|
function postUpdateStatePath(config) {
|
|
8751
|
-
return (0,
|
|
9513
|
+
return (0, import_node_path7.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
|
|
8752
9514
|
}
|
|
8753
9515
|
async function resolveUpdateRuntime(runtime) {
|
|
8754
9516
|
if (runtime !== "auto") {
|
|
@@ -8778,18 +9540,18 @@ async function runtimeThreadnoteBin(runtime) {
|
|
|
8778
9540
|
if (runtime === "npm") {
|
|
8779
9541
|
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
8780
9542
|
const prefix = result.stdout.trim();
|
|
8781
|
-
return prefix ? (0,
|
|
9543
|
+
return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
8782
9544
|
}
|
|
8783
9545
|
if (runtime === "bun") {
|
|
8784
9546
|
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
8785
9547
|
const binDir = result.stdout.trim();
|
|
8786
|
-
return binDir ? (0,
|
|
9548
|
+
return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
8787
9549
|
}
|
|
8788
|
-
return (0,
|
|
9550
|
+
return (0, import_node_path7.join)(process.env.DENO_INSTALL ?? (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
|
|
8789
9551
|
}
|
|
8790
9552
|
async function isExecutable(path) {
|
|
8791
9553
|
try {
|
|
8792
|
-
await (0,
|
|
9554
|
+
await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
|
|
8793
9555
|
return true;
|
|
8794
9556
|
} catch (_err) {
|
|
8795
9557
|
return false;
|
|
@@ -8867,7 +9629,7 @@ function safeVersionNumber(value) {
|
|
|
8867
9629
|
async function runDoctor(config, options) {
|
|
8868
9630
|
const checks = [];
|
|
8869
9631
|
checks.push({ name: "mode", status: "ok", detail: options.dryRun ? "dry run; no writes" : "read-only checks" });
|
|
8870
|
-
checks.push({ name: "platform", status: (0,
|
|
9632
|
+
checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
|
|
8871
9633
|
checks.push(await commandCheck("node", ["--version"]));
|
|
8872
9634
|
checks.push(await commandCheck("python3", ["--version"]));
|
|
8873
9635
|
checks.push(await commandCheck("openviking-server", ["--help"]));
|
|
@@ -8880,9 +9642,9 @@ async function runDoctor(config, options) {
|
|
|
8880
9642
|
checks.push(await commandShimCheck());
|
|
8881
9643
|
checks.push(...await userAgentInstructionsChecks());
|
|
8882
9644
|
checks.push(await manifestCheck(config.manifestPath));
|
|
8883
|
-
checks.push(await fileCheck((0,
|
|
8884
|
-
checks.push(await fileCheck((0,
|
|
8885
|
-
checks.push(await fileCheck((0,
|
|
9645
|
+
checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
|
|
9646
|
+
checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
|
|
9647
|
+
checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
|
|
8886
9648
|
checks.push(await healthCheck(config));
|
|
8887
9649
|
for (const check of checks) {
|
|
8888
9650
|
console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
|
|
@@ -8899,9 +9661,9 @@ async function runInstall(config, options) {
|
|
|
8899
9661
|
const repairInvalidConfigs = options.repairInvalidConfigs === true;
|
|
8900
9662
|
const dryRun = options.dryRun === true;
|
|
8901
9663
|
await ensureDirectory(config.agentContextHome, dryRun);
|
|
8902
|
-
await ensureDirectory((0,
|
|
8903
|
-
await ensureDirectory((0,
|
|
8904
|
-
await ensureDirectory((0,
|
|
9664
|
+
await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "logs"), dryRun);
|
|
9665
|
+
await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "redacted"), dryRun);
|
|
9666
|
+
await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "mcp"), dryRun);
|
|
8905
9667
|
await installCommandShim(dryRun);
|
|
8906
9668
|
await installUserAgentInstructions(dryRun);
|
|
8907
9669
|
const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
@@ -8927,17 +9689,17 @@ async function runInstall(config, options) {
|
|
|
8927
9689
|
}
|
|
8928
9690
|
await writeTemplateIfMissing({
|
|
8929
9691
|
config,
|
|
8930
|
-
destinationPath: (0,
|
|
9692
|
+
destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ov.conf"),
|
|
8931
9693
|
dryRun,
|
|
8932
9694
|
shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
8933
|
-
templatePath: (0,
|
|
9695
|
+
templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json")
|
|
8934
9696
|
});
|
|
8935
9697
|
await writeTemplateIfMissing({
|
|
8936
9698
|
config,
|
|
8937
|
-
destinationPath: (0,
|
|
9699
|
+
destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ovcli.conf"),
|
|
8938
9700
|
dryRun,
|
|
8939
9701
|
shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
8940
|
-
templatePath: (0,
|
|
9702
|
+
templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json")
|
|
8941
9703
|
});
|
|
8942
9704
|
if (options.start !== false) {
|
|
8943
9705
|
const healthy = await repairServerHealth(config, dryRun);
|
|
@@ -8987,7 +9749,7 @@ async function runUninstall(config, options) {
|
|
|
8987
9749
|
}
|
|
8988
9750
|
console.log("Uninstalling local Threadnote setup.");
|
|
8989
9751
|
await runStop(config, { dryRun });
|
|
8990
|
-
await removePathIfExists((0,
|
|
9752
|
+
await removePathIfExists((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
|
|
8991
9753
|
await removeLaunchAgent(dryRun);
|
|
8992
9754
|
await removeMcpConfigs(options.mcp ?? "available", dryRun);
|
|
8993
9755
|
await removeMcpSnippets(config, dryRun);
|
|
@@ -9041,16 +9803,16 @@ async function repairManifest(config, dryRun) {
|
|
|
9041
9803
|
console.log(output2.trimEnd());
|
|
9042
9804
|
return;
|
|
9043
9805
|
}
|
|
9044
|
-
await ensureDirectory((0,
|
|
9806
|
+
await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
|
|
9045
9807
|
const currentContent = await readFileIfExists(config.manifestPath);
|
|
9046
9808
|
if (currentContent !== void 0) {
|
|
9047
9809
|
const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
|
|
9048
|
-
await (0,
|
|
9049
|
-
await (0,
|
|
9810
|
+
await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
9811
|
+
await (0, import_promises9.chmod)(backupPath, 384);
|
|
9050
9812
|
console.log(`Backup: ${backupPath}`);
|
|
9051
9813
|
}
|
|
9052
|
-
await (0,
|
|
9053
|
-
await (0,
|
|
9814
|
+
await (0, import_promises9.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
9815
|
+
await (0, import_promises9.chmod)(config.manifestPath, 384);
|
|
9054
9816
|
console.log(`Wrote replacement manifest: ${config.manifestPath}`);
|
|
9055
9817
|
}
|
|
9056
9818
|
async function repairServerHealth(config, dryRun) {
|
|
@@ -9091,7 +9853,7 @@ async function runStart(config, options) {
|
|
|
9091
9853
|
);
|
|
9092
9854
|
}
|
|
9093
9855
|
const logPath = openVikingLogPath(config);
|
|
9094
|
-
await ensureDirectory((0,
|
|
9856
|
+
await ensureDirectory((0, import_node_path8.dirname)(logPath), false);
|
|
9095
9857
|
if (options.foreground === true) {
|
|
9096
9858
|
const result = await runInteractive(server, args);
|
|
9097
9859
|
process.exitCode = result;
|
|
@@ -9100,7 +9862,7 @@ async function runStart(config, options) {
|
|
|
9100
9862
|
const logFd = (0, import_node_fs5.openSync)(logPath, "a");
|
|
9101
9863
|
const child = spawnDetachedServerWithLog(server, args, logFd);
|
|
9102
9864
|
child.unref();
|
|
9103
|
-
await (0,
|
|
9865
|
+
await (0, import_promises9.writeFile)((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
|
|
9104
9866
|
`, "utf8");
|
|
9105
9867
|
const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
|
|
9106
9868
|
if (health) {
|
|
@@ -9126,14 +9888,14 @@ function spawnDetachedServer(server, args, logFd) {
|
|
|
9126
9888
|
}
|
|
9127
9889
|
async function runStop(config, options) {
|
|
9128
9890
|
const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
9129
|
-
if ((0,
|
|
9891
|
+
if ((0, import_node_os6.platform)() === "darwin") {
|
|
9130
9892
|
if (options.dryRun === true || await exists(launchAgentPath)) {
|
|
9131
9893
|
await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
|
|
9132
9894
|
} else {
|
|
9133
9895
|
console.log(`No LaunchAgent found: ${launchAgentPath}`);
|
|
9134
9896
|
}
|
|
9135
9897
|
}
|
|
9136
|
-
const pidPath = (0,
|
|
9898
|
+
const pidPath = (0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid");
|
|
9137
9899
|
const pidText = await readFileIfExists(pidPath);
|
|
9138
9900
|
if (!pidText) {
|
|
9139
9901
|
console.log("No pid file found for detached OpenViking server.");
|
|
@@ -9229,7 +9991,7 @@ async function pythonSystemCertificatesCheck() {
|
|
|
9229
9991
|
};
|
|
9230
9992
|
}
|
|
9231
9993
|
async function commandShimCheck() {
|
|
9232
|
-
const shimPath = (0,
|
|
9994
|
+
const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
9233
9995
|
const content = await readFileIfExists(shimPath);
|
|
9234
9996
|
if (content === void 0) {
|
|
9235
9997
|
return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
|
|
@@ -9295,11 +10057,11 @@ async function hasPythonModule(serverPath, moduleName) {
|
|
|
9295
10057
|
async function siblingPythonForExecutable(executablePath) {
|
|
9296
10058
|
let resolvedPath;
|
|
9297
10059
|
try {
|
|
9298
|
-
resolvedPath = await (0,
|
|
10060
|
+
resolvedPath = await (0, import_promises9.realpath)(executablePath);
|
|
9299
10061
|
} catch (_err) {
|
|
9300
10062
|
return void 0;
|
|
9301
10063
|
}
|
|
9302
|
-
const pythonPath = (0,
|
|
10064
|
+
const pythonPath = (0, import_node_path8.join)((0, import_node_path8.dirname)(resolvedPath), "python");
|
|
9303
10065
|
return await exists(pythonPath) ? pythonPath : void 0;
|
|
9304
10066
|
}
|
|
9305
10067
|
async function manifestCheck(path) {
|
|
@@ -9421,38 +10183,38 @@ function printInstallNextSteps(options) {
|
|
|
9421
10183
|
}
|
|
9422
10184
|
async function writeTemplateIfMissing(options) {
|
|
9423
10185
|
if (await exists(options.destinationPath)) {
|
|
9424
|
-
const currentContent = await (0,
|
|
10186
|
+
const currentContent = await (0, import_promises9.readFile)(options.destinationPath, "utf8");
|
|
9425
10187
|
if (options.shouldRepair?.(currentContent) !== true) {
|
|
9426
10188
|
console.log(`Already exists: ${options.destinationPath}`);
|
|
9427
10189
|
return;
|
|
9428
10190
|
}
|
|
9429
|
-
const rendered2 = renderTemplate(await (0,
|
|
10191
|
+
const rendered2 = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
|
|
9430
10192
|
if (options.dryRun) {
|
|
9431
10193
|
console.log(`Would repair generated config: ${options.destinationPath}`);
|
|
9432
10194
|
return;
|
|
9433
10195
|
}
|
|
9434
10196
|
const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
|
|
9435
|
-
await (0,
|
|
9436
|
-
await (0,
|
|
9437
|
-
await (0,
|
|
9438
|
-
await (0,
|
|
10197
|
+
await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
10198
|
+
await (0, import_promises9.chmod)(backupPath, 384);
|
|
10199
|
+
await (0, import_promises9.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
|
|
10200
|
+
await (0, import_promises9.chmod)(options.destinationPath, 384);
|
|
9439
10201
|
console.log(`Repaired generated config: ${options.destinationPath}`);
|
|
9440
10202
|
console.log(`Backup: ${backupPath}`);
|
|
9441
10203
|
return;
|
|
9442
10204
|
}
|
|
9443
|
-
const rendered = renderTemplate(await (0,
|
|
10205
|
+
const rendered = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
|
|
9444
10206
|
if (options.dryRun) {
|
|
9445
10207
|
console.log(`Would write ${options.destinationPath}`);
|
|
9446
10208
|
return;
|
|
9447
10209
|
}
|
|
9448
|
-
await ensureDirectory((0,
|
|
9449
|
-
await (0,
|
|
9450
|
-
await (0,
|
|
10210
|
+
await ensureDirectory((0, import_node_path8.dirname)(options.destinationPath), false);
|
|
10211
|
+
await (0, import_promises9.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
|
|
10212
|
+
await (0, import_promises9.chmod)(options.destinationPath, 384);
|
|
9451
10213
|
console.log(`Wrote ${options.destinationPath}`);
|
|
9452
10214
|
}
|
|
9453
10215
|
async function installCommandShim(dryRun) {
|
|
9454
10216
|
const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
|
|
9455
|
-
const shimPath = (0,
|
|
10217
|
+
const shimPath = (0, import_node_path8.join)(binDir, "threadnote");
|
|
9456
10218
|
const existingContent = await readFileIfExists(shimPath);
|
|
9457
10219
|
if (existingContent && !isManagedCommandShim(existingContent)) {
|
|
9458
10220
|
console.log(`WARN not overwriting existing command shim: ${shimPath}`);
|
|
@@ -9468,12 +10230,12 @@ async function installCommandShim(dryRun) {
|
|
|
9468
10230
|
return;
|
|
9469
10231
|
}
|
|
9470
10232
|
await ensureDirectory(binDir, false);
|
|
9471
|
-
await (0,
|
|
9472
|
-
await (0,
|
|
10233
|
+
await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
|
|
10234
|
+
await (0, import_promises9.chmod)(shimPath, 493);
|
|
9473
10235
|
console.log(`Wrote command shim: ${shimPath}`);
|
|
9474
10236
|
}
|
|
9475
10237
|
async function removeCommandShim(dryRun) {
|
|
9476
|
-
const shimPath = (0,
|
|
10238
|
+
const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
9477
10239
|
const content = await readFileIfExists(shimPath);
|
|
9478
10240
|
if (content === void 0) {
|
|
9479
10241
|
console.log(`Already absent: ${shimPath}`);
|
|
@@ -9507,8 +10269,8 @@ async function installUserAgentInstructions(dryRun) {
|
|
|
9507
10269
|
console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
|
|
9508
10270
|
continue;
|
|
9509
10271
|
}
|
|
9510
|
-
await ensureDirectory((0,
|
|
9511
|
-
await (0,
|
|
10272
|
+
await ensureDirectory((0, import_node_path8.dirname)(targetPath), false);
|
|
10273
|
+
await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
9512
10274
|
console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
|
|
9513
10275
|
}
|
|
9514
10276
|
}
|
|
@@ -9545,7 +10307,7 @@ async function removeUserAgentInstructions(dryRun) {
|
|
|
9545
10307
|
console.log(`Would update ${targetPath}`);
|
|
9546
10308
|
continue;
|
|
9547
10309
|
}
|
|
9548
|
-
await (0,
|
|
10310
|
+
await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
9549
10311
|
console.log(`Updated ${targetPath}`);
|
|
9550
10312
|
}
|
|
9551
10313
|
}
|
|
@@ -9565,7 +10327,7 @@ async function renderUserAgentInstructions(target) {
|
|
|
9565
10327
|
].join("\n");
|
|
9566
10328
|
}
|
|
9567
10329
|
async function renderUserAgentInstructionsBlock() {
|
|
9568
|
-
const instructions = (await (0,
|
|
10330
|
+
const instructions = (await (0, import_promises9.readFile)((0, import_node_path8.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
|
|
9569
10331
|
return `${USER_INSTRUCTIONS_START_MARKER}
|
|
9570
10332
|
${instructions}
|
|
9571
10333
|
${USER_INSTRUCTIONS_END_MARKER}`;
|
|
@@ -9640,12 +10402,12 @@ function isManagedCommandShim(content) {
|
|
|
9640
10402
|
return content.includes(SHIM_MARKER);
|
|
9641
10403
|
}
|
|
9642
10404
|
async function installLaunchAgent(config, dryRun) {
|
|
9643
|
-
if ((0,
|
|
10405
|
+
if ((0, import_node_os6.platform)() !== "darwin") {
|
|
9644
10406
|
throw new Error("launchd autostart is only supported on macOS.");
|
|
9645
10407
|
}
|
|
9646
|
-
const source = (0,
|
|
10408
|
+
const source = (0, import_node_path8.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
|
|
9647
10409
|
const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
9648
|
-
const rendered = renderTemplate(await (0,
|
|
10410
|
+
const rendered = renderTemplate(await (0, import_promises9.readFile)(source, "utf8"), config);
|
|
9649
10411
|
if (dryRun) {
|
|
9650
10412
|
console.log(`Would write ${destination}`);
|
|
9651
10413
|
console.log(`Would run: launchctl unload ${destination}`);
|
|
@@ -9653,9 +10415,9 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
9653
10415
|
console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
|
|
9654
10416
|
return;
|
|
9655
10417
|
}
|
|
9656
|
-
await ensureDirectory((0,
|
|
9657
|
-
await ensureDirectory((0,
|
|
9658
|
-
await (0,
|
|
10418
|
+
await ensureDirectory((0, import_node_path8.dirname)(destination), false);
|
|
10419
|
+
await ensureDirectory((0, import_node_path8.dirname)(openVikingLogPath(config)), false);
|
|
10420
|
+
await (0, import_promises9.writeFile)(destination, rendered, "utf8");
|
|
9659
10421
|
await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
|
|
9660
10422
|
await maybeRun(false, "launchctl", ["load", destination]);
|
|
9661
10423
|
await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
|
|
@@ -9718,7 +10480,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
|
|
|
9718
10480
|
if (typeof parsed.default_user !== "string") {
|
|
9719
10481
|
return false;
|
|
9720
10482
|
}
|
|
9721
|
-
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0,
|
|
10483
|
+
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path8.join)(config.agentContextHome, "data")) {
|
|
9722
10484
|
return false;
|
|
9723
10485
|
}
|
|
9724
10486
|
return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
|
|
@@ -9825,6 +10587,31 @@ async function main() {
|
|
|
9825
10587
|
program2.command("forget").description("Remove a viking:// URI from local OpenViking context").argument("<uri>", "viking:// URI to remove").option("--dry-run", "Print ov command without deleting").action(async (uri, options) => {
|
|
9826
10588
|
await runForget(getRuntimeConfig(program2), uri, options);
|
|
9827
10589
|
});
|
|
10590
|
+
const share = program2.command("share").description("Share durable memories with teammates through a git-backed repository");
|
|
10591
|
+
share.command("init").description("Configure a shared memories repo for a team (clones the remote into the local memory tree)").argument("<remote-url>", "git remote URL of the shared memories repo").option("--team <name>", 'Team name; defaults to "default"').option("--set-default", "Mark this team as the default for future share commands").option(
|
|
10592
|
+
"--no-push",
|
|
10593
|
+
"Do not push the auto-generated .gitignore housekeeping commit. Does not affect future publishes."
|
|
10594
|
+
).option("--dry-run", "Print actions without running them").action(async (remoteUrl, options) => {
|
|
10595
|
+
await runShareInit(getRuntimeConfig(program2), remoteUrl, options);
|
|
10596
|
+
});
|
|
10597
|
+
share.command("status").description("Show git status and ahead/behind counts for a shared team").option("--team <name>", "Team name; defaults to the configured default team").option("--dry-run", "Print git commands without running them").action(async (options) => {
|
|
10598
|
+
await runShareStatus(getRuntimeConfig(program2), options);
|
|
10599
|
+
});
|
|
10600
|
+
share.command("sync").description("Pull, reindex, and push the shared memories repo for a team").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message when auto-committing local edits").option("--no-auto-commit", "Refuse to sync if there are uncommitted local changes").option("--no-push", "Skip the push step after pulling and reindexing").option("--dry-run", "Print actions without running them").action(async (options) => {
|
|
10601
|
+
await runShareSync(getRuntimeConfig(program2), options);
|
|
10602
|
+
});
|
|
10603
|
+
share.command("publish").description("Move a personal memory into the shared team namespace, commit and push").argument("<viking-uri>", "viking:// memory URI to publish").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) => {
|
|
10604
|
+
await runSharePublish(getRuntimeConfig(program2), uri, options);
|
|
10605
|
+
});
|
|
10606
|
+
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) => {
|
|
10607
|
+
await runShareUnpublish(getRuntimeConfig(program2), uri, options);
|
|
10608
|
+
});
|
|
10609
|
+
share.command("list").description("List configured shared teams").option("--dry-run", "Print without side effects (no-op for list)").action(async (options) => {
|
|
10610
|
+
await runShareList(getRuntimeConfig(program2), options);
|
|
10611
|
+
});
|
|
10612
|
+
share.command("remove").description("Forget a configured team and optionally delete its worktree/gitdir").option("--team <name>", "Team name; defaults to the configured default team").option("--keep-files", "Keep the worktree and gitdir on disk; only forget the team entry").option("--dry-run", "Print actions without running them").action(async (options) => {
|
|
10613
|
+
await runShareRemove(getRuntimeConfig(program2), options);
|
|
10614
|
+
});
|
|
9828
10615
|
program2.command("export-pack").description("Export local OpenViking context to an .ovpack").option("--dry-run", "Print ov command without exporting").option("--path <path>", "Output .ovpack path").option("--uri <uri>", "Source viking:// URI to export", "viking://").action(async (options) => {
|
|
9829
10616
|
await runExportPack(getRuntimeConfig(program2), options);
|
|
9830
10617
|
});
|