threadnote 0.3.8 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/mcp_server.cjs +4082 -999
- package/dist/threadnote.cjs +931 -70
- 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,782 @@ 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");
|
|
9166
|
+
var import_node_process2 = require("node:process");
|
|
9167
|
+
var import_promises10 = require("node:readline/promises");
|
|
8404
9168
|
|
|
8405
9169
|
// src/update.ts
|
|
8406
9170
|
var import_node_fs4 = require("node:fs");
|
|
8407
|
-
var
|
|
8408
|
-
var
|
|
8409
|
-
var
|
|
8410
|
-
var
|
|
9171
|
+
var import_promises7 = require("node:fs/promises");
|
|
9172
|
+
var import_node_os5 = require("node:os");
|
|
9173
|
+
var import_node_path7 = require("node:path");
|
|
9174
|
+
var import_promises8 = require("node:readline/promises");
|
|
8411
9175
|
var import_node_process = require("node:process");
|
|
8412
9176
|
var NPM_PACKAGE_NAME = "threadnote";
|
|
8413
9177
|
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
@@ -8555,7 +9319,7 @@ async function getUpdateInfo(config, options) {
|
|
|
8555
9319
|
};
|
|
8556
9320
|
}
|
|
8557
9321
|
async function currentPackageVersion() {
|
|
8558
|
-
const rawPackage = await (0,
|
|
9322
|
+
const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
|
|
8559
9323
|
const parsed = JSON.parse(rawPackage);
|
|
8560
9324
|
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8561
9325
|
throw new Error("Could not read current threadnote package version.");
|
|
@@ -8605,11 +9369,11 @@ async function readFreshCache(config, registry) {
|
|
|
8605
9369
|
}
|
|
8606
9370
|
async function writeUpdateCache(config, cache) {
|
|
8607
9371
|
await ensureDirectory(config.agentContextHome, false);
|
|
8608
|
-
await (0,
|
|
9372
|
+
await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
|
|
8609
9373
|
`, { encoding: "utf8", mode: 384 });
|
|
8610
9374
|
}
|
|
8611
9375
|
function updateCachePath(config) {
|
|
8612
|
-
return (0,
|
|
9376
|
+
return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
|
|
8613
9377
|
}
|
|
8614
9378
|
async function runApplicablePostUpdateMigrations(config, options) {
|
|
8615
9379
|
const state = await readPostUpdateState(config);
|
|
@@ -8673,7 +9437,7 @@ async function applicablePostUpdateMigrations(config, options) {
|
|
|
8673
9437
|
return applicable;
|
|
8674
9438
|
}
|
|
8675
9439
|
async function readPostUpdateMigrations() {
|
|
8676
|
-
const raw = await readFileIfExists((0,
|
|
9440
|
+
const raw = await readFileIfExists((0, import_node_path7.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
|
|
8677
9441
|
if (!raw) {
|
|
8678
9442
|
return [];
|
|
8679
9443
|
}
|
|
@@ -8712,7 +9476,7 @@ function printPostUpdateMigration(migration) {
|
|
|
8712
9476
|
}
|
|
8713
9477
|
}
|
|
8714
9478
|
async function confirmPostUpdateMigration(prompt) {
|
|
8715
|
-
const readline = (0,
|
|
9479
|
+
const readline = (0, import_promises8.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
8716
9480
|
try {
|
|
8717
9481
|
const answer = (await readline.question(prompt)).trim().toLowerCase();
|
|
8718
9482
|
return answer === "y" || answer === "yes";
|
|
@@ -8744,11 +9508,11 @@ async function readPostUpdateState(config) {
|
|
|
8744
9508
|
}
|
|
8745
9509
|
async function writePostUpdateState(config, state) {
|
|
8746
9510
|
await ensureDirectory(config.agentContextHome, false);
|
|
8747
|
-
await (0,
|
|
9511
|
+
await (0, import_promises7.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
|
|
8748
9512
|
`, { encoding: "utf8", mode: 384 });
|
|
8749
9513
|
}
|
|
8750
9514
|
function postUpdateStatePath(config) {
|
|
8751
|
-
return (0,
|
|
9515
|
+
return (0, import_node_path7.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
|
|
8752
9516
|
}
|
|
8753
9517
|
async function resolveUpdateRuntime(runtime) {
|
|
8754
9518
|
if (runtime !== "auto") {
|
|
@@ -8778,18 +9542,18 @@ async function runtimeThreadnoteBin(runtime) {
|
|
|
8778
9542
|
if (runtime === "npm") {
|
|
8779
9543
|
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
8780
9544
|
const prefix = result.stdout.trim();
|
|
8781
|
-
return prefix ? (0,
|
|
9545
|
+
return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
8782
9546
|
}
|
|
8783
9547
|
if (runtime === "bun") {
|
|
8784
9548
|
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
8785
9549
|
const binDir = result.stdout.trim();
|
|
8786
|
-
return binDir ? (0,
|
|
9550
|
+
return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
8787
9551
|
}
|
|
8788
|
-
return (0,
|
|
9552
|
+
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
9553
|
}
|
|
8790
9554
|
async function isExecutable(path) {
|
|
8791
9555
|
try {
|
|
8792
|
-
await (0,
|
|
9556
|
+
await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
|
|
8793
9557
|
return true;
|
|
8794
9558
|
} catch (_err) {
|
|
8795
9559
|
return false;
|
|
@@ -8867,7 +9631,7 @@ function safeVersionNumber(value) {
|
|
|
8867
9631
|
async function runDoctor(config, options) {
|
|
8868
9632
|
const checks = [];
|
|
8869
9633
|
checks.push({ name: "mode", status: "ok", detail: options.dryRun ? "dry run; no writes" : "read-only checks" });
|
|
8870
|
-
checks.push({ name: "platform", status: (0,
|
|
9634
|
+
checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
|
|
8871
9635
|
checks.push(await commandCheck("node", ["--version"]));
|
|
8872
9636
|
checks.push(await commandCheck("python3", ["--version"]));
|
|
8873
9637
|
checks.push(await commandCheck("openviking-server", ["--help"]));
|
|
@@ -8880,9 +9644,9 @@ async function runDoctor(config, options) {
|
|
|
8880
9644
|
checks.push(await commandShimCheck());
|
|
8881
9645
|
checks.push(...await userAgentInstructionsChecks());
|
|
8882
9646
|
checks.push(await manifestCheck(config.manifestPath));
|
|
8883
|
-
checks.push(await fileCheck((0,
|
|
8884
|
-
checks.push(await fileCheck((0,
|
|
8885
|
-
checks.push(await fileCheck((0,
|
|
9647
|
+
checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
|
|
9648
|
+
checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
|
|
9649
|
+
checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
|
|
8886
9650
|
checks.push(await healthCheck(config));
|
|
8887
9651
|
for (const check of checks) {
|
|
8888
9652
|
console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
|
|
@@ -8899,9 +9663,9 @@ async function runInstall(config, options) {
|
|
|
8899
9663
|
const repairInvalidConfigs = options.repairInvalidConfigs === true;
|
|
8900
9664
|
const dryRun = options.dryRun === true;
|
|
8901
9665
|
await ensureDirectory(config.agentContextHome, dryRun);
|
|
8902
|
-
await ensureDirectory((0,
|
|
8903
|
-
await ensureDirectory((0,
|
|
8904
|
-
await ensureDirectory((0,
|
|
9666
|
+
await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "logs"), dryRun);
|
|
9667
|
+
await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "redacted"), dryRun);
|
|
9668
|
+
await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "mcp"), dryRun);
|
|
8905
9669
|
await installCommandShim(dryRun);
|
|
8906
9670
|
await installUserAgentInstructions(dryRun);
|
|
8907
9671
|
const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
@@ -8927,17 +9691,17 @@ async function runInstall(config, options) {
|
|
|
8927
9691
|
}
|
|
8928
9692
|
await writeTemplateIfMissing({
|
|
8929
9693
|
config,
|
|
8930
|
-
destinationPath: (0,
|
|
9694
|
+
destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ov.conf"),
|
|
8931
9695
|
dryRun,
|
|
8932
9696
|
shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
8933
|
-
templatePath: (0,
|
|
9697
|
+
templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json")
|
|
8934
9698
|
});
|
|
8935
9699
|
await writeTemplateIfMissing({
|
|
8936
9700
|
config,
|
|
8937
|
-
destinationPath: (0,
|
|
9701
|
+
destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ovcli.conf"),
|
|
8938
9702
|
dryRun,
|
|
8939
9703
|
shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
8940
|
-
templatePath: (0,
|
|
9704
|
+
templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json")
|
|
8941
9705
|
});
|
|
8942
9706
|
if (options.start !== false) {
|
|
8943
9707
|
const healthy = await repairServerHealth(config, dryRun);
|
|
@@ -8987,7 +9751,7 @@ async function runUninstall(config, options) {
|
|
|
8987
9751
|
}
|
|
8988
9752
|
console.log("Uninstalling local Threadnote setup.");
|
|
8989
9753
|
await runStop(config, { dryRun });
|
|
8990
|
-
await removePathIfExists((0,
|
|
9754
|
+
await removePathIfExists((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
|
|
8991
9755
|
await removeLaunchAgent(dryRun);
|
|
8992
9756
|
await removeMcpConfigs(options.mcp ?? "available", dryRun);
|
|
8993
9757
|
await removeMcpSnippets(config, dryRun);
|
|
@@ -9041,16 +9805,16 @@ async function repairManifest(config, dryRun) {
|
|
|
9041
9805
|
console.log(output2.trimEnd());
|
|
9042
9806
|
return;
|
|
9043
9807
|
}
|
|
9044
|
-
await ensureDirectory((0,
|
|
9808
|
+
await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
|
|
9045
9809
|
const currentContent = await readFileIfExists(config.manifestPath);
|
|
9046
9810
|
if (currentContent !== void 0) {
|
|
9047
9811
|
const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
|
|
9048
|
-
await (0,
|
|
9049
|
-
await (0,
|
|
9812
|
+
await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
9813
|
+
await (0, import_promises9.chmod)(backupPath, 384);
|
|
9050
9814
|
console.log(`Backup: ${backupPath}`);
|
|
9051
9815
|
}
|
|
9052
|
-
await (0,
|
|
9053
|
-
await (0,
|
|
9816
|
+
await (0, import_promises9.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
9817
|
+
await (0, import_promises9.chmod)(config.manifestPath, 384);
|
|
9054
9818
|
console.log(`Wrote replacement manifest: ${config.manifestPath}`);
|
|
9055
9819
|
}
|
|
9056
9820
|
async function repairServerHealth(config, dryRun) {
|
|
@@ -9091,7 +9855,7 @@ async function runStart(config, options) {
|
|
|
9091
9855
|
);
|
|
9092
9856
|
}
|
|
9093
9857
|
const logPath = openVikingLogPath(config);
|
|
9094
|
-
await ensureDirectory((0,
|
|
9858
|
+
await ensureDirectory((0, import_node_path8.dirname)(logPath), false);
|
|
9095
9859
|
if (options.foreground === true) {
|
|
9096
9860
|
const result = await runInteractive(server, args);
|
|
9097
9861
|
process.exitCode = result;
|
|
@@ -9100,7 +9864,7 @@ async function runStart(config, options) {
|
|
|
9100
9864
|
const logFd = (0, import_node_fs5.openSync)(logPath, "a");
|
|
9101
9865
|
const child = spawnDetachedServerWithLog(server, args, logFd);
|
|
9102
9866
|
child.unref();
|
|
9103
|
-
await (0,
|
|
9867
|
+
await (0, import_promises9.writeFile)((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
|
|
9104
9868
|
`, "utf8");
|
|
9105
9869
|
const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
|
|
9106
9870
|
if (health) {
|
|
@@ -9126,14 +9890,14 @@ function spawnDetachedServer(server, args, logFd) {
|
|
|
9126
9890
|
}
|
|
9127
9891
|
async function runStop(config, options) {
|
|
9128
9892
|
const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
9129
|
-
if ((0,
|
|
9893
|
+
if ((0, import_node_os6.platform)() === "darwin") {
|
|
9130
9894
|
if (options.dryRun === true || await exists(launchAgentPath)) {
|
|
9131
9895
|
await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
|
|
9132
9896
|
} else {
|
|
9133
9897
|
console.log(`No LaunchAgent found: ${launchAgentPath}`);
|
|
9134
9898
|
}
|
|
9135
9899
|
}
|
|
9136
|
-
const pidPath = (0,
|
|
9900
|
+
const pidPath = (0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid");
|
|
9137
9901
|
const pidText = await readFileIfExists(pidPath);
|
|
9138
9902
|
if (!pidText) {
|
|
9139
9903
|
console.log("No pid file found for detached OpenViking server.");
|
|
@@ -9229,7 +9993,7 @@ async function pythonSystemCertificatesCheck() {
|
|
|
9229
9993
|
};
|
|
9230
9994
|
}
|
|
9231
9995
|
async function commandShimCheck() {
|
|
9232
|
-
const shimPath = (0,
|
|
9996
|
+
const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
9233
9997
|
const content = await readFileIfExists(shimPath);
|
|
9234
9998
|
if (content === void 0) {
|
|
9235
9999
|
return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
|
|
@@ -9295,11 +10059,11 @@ async function hasPythonModule(serverPath, moduleName) {
|
|
|
9295
10059
|
async function siblingPythonForExecutable(executablePath) {
|
|
9296
10060
|
let resolvedPath;
|
|
9297
10061
|
try {
|
|
9298
|
-
resolvedPath = await (0,
|
|
10062
|
+
resolvedPath = await (0, import_promises9.realpath)(executablePath);
|
|
9299
10063
|
} catch (_err) {
|
|
9300
10064
|
return void 0;
|
|
9301
10065
|
}
|
|
9302
|
-
const pythonPath = (0,
|
|
10066
|
+
const pythonPath = (0, import_node_path8.join)((0, import_node_path8.dirname)(resolvedPath), "python");
|
|
9303
10067
|
return await exists(pythonPath) ? pythonPath : void 0;
|
|
9304
10068
|
}
|
|
9305
10069
|
async function manifestCheck(path) {
|
|
@@ -9348,11 +10112,83 @@ async function waitForOpenVikingHealth(config, timeoutMs) {
|
|
|
9348
10112
|
return void 0;
|
|
9349
10113
|
}
|
|
9350
10114
|
async function runInstallCommands(config, preferred, force, dryRun) {
|
|
9351
|
-
|
|
10115
|
+
let manager = preferred;
|
|
10116
|
+
if (manager === void 0 && !dryRun) {
|
|
10117
|
+
const detected = await detectPackageManager();
|
|
10118
|
+
if (detected === "pip" && await offerToInstallUv()) {
|
|
10119
|
+
const rediscovered = await detectPackageManager();
|
|
10120
|
+
if (rediscovered === "uv") {
|
|
10121
|
+
manager = "uv";
|
|
10122
|
+
}
|
|
10123
|
+
}
|
|
10124
|
+
}
|
|
10125
|
+
const installCommands = await getInstallCommands(config, manager, force);
|
|
9352
10126
|
for (const installCommand of installCommands) {
|
|
9353
10127
|
await maybeRun(dryRun, installCommand.executable, installCommand.args);
|
|
9354
10128
|
}
|
|
9355
10129
|
}
|
|
10130
|
+
async function offerToInstallUv() {
|
|
10131
|
+
if (import_node_process2.stdin.isTTY !== true || import_node_process2.stdout.isTTY !== true) {
|
|
10132
|
+
console.warn(
|
|
10133
|
+
"Neither uv nor pipx was found on PATH. Falling back to `python3 -m pip install --user`, which fails on PEP 668 (Homebrew/system) Python.\nRe-run with --package-manager uv after installing uv (brew install uv), or pass --package-manager pipx."
|
|
10134
|
+
);
|
|
10135
|
+
return false;
|
|
10136
|
+
}
|
|
10137
|
+
const readline = (0, import_promises10.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
10138
|
+
let answer;
|
|
10139
|
+
try {
|
|
10140
|
+
answer = (await readline.question(
|
|
10141
|
+
"OpenViking installs into Python; neither uv nor pipx is on PATH so threadnote would fall back to `pip install --user`, which fails on PEP 668 setups.\nInstall uv now? [Y/n] "
|
|
10142
|
+
)).trim().toLowerCase();
|
|
10143
|
+
} finally {
|
|
10144
|
+
readline.close();
|
|
10145
|
+
}
|
|
10146
|
+
if (answer === "n" || answer === "no") {
|
|
10147
|
+
console.log("Continuing with `python3 -m pip install --user`. You may hit PEP 668 errors on managed Pythons.");
|
|
10148
|
+
return false;
|
|
10149
|
+
}
|
|
10150
|
+
return await installUv();
|
|
10151
|
+
}
|
|
10152
|
+
async function installUv() {
|
|
10153
|
+
const brew = await findExecutable(["brew"]);
|
|
10154
|
+
if (brew) {
|
|
10155
|
+
console.log("Installing uv via Homebrew...");
|
|
10156
|
+
const result = await runCommand(brew, ["install", "uv"], { allowFailure: true });
|
|
10157
|
+
if (result.exitCode === 0) {
|
|
10158
|
+
if (result.stdout.trim()) {
|
|
10159
|
+
console.log(result.stdout.trim());
|
|
10160
|
+
}
|
|
10161
|
+
if (await findExecutable(["uv"])) {
|
|
10162
|
+
return true;
|
|
10163
|
+
}
|
|
10164
|
+
} else {
|
|
10165
|
+
console.warn(`brew install uv failed: ${(result.stderr || result.stdout).trim()}`);
|
|
10166
|
+
}
|
|
10167
|
+
}
|
|
10168
|
+
if (await findExecutable(["curl"]) && await findExecutable(["sh"])) {
|
|
10169
|
+
console.log("Installing uv via the official install script (curl -LsSf https://astral.sh/uv/install.sh | sh)...");
|
|
10170
|
+
const result = await runCommand("sh", ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"], {
|
|
10171
|
+
allowFailure: true
|
|
10172
|
+
});
|
|
10173
|
+
if (result.exitCode === 0) {
|
|
10174
|
+
if (result.stdout.trim()) {
|
|
10175
|
+
console.log(result.stdout.trim());
|
|
10176
|
+
}
|
|
10177
|
+
if (await findExecutable(["uv"])) {
|
|
10178
|
+
return true;
|
|
10179
|
+
}
|
|
10180
|
+
console.warn(
|
|
10181
|
+
"uv installed, but the new binary is not yet on this shell PATH. Open a new shell (or `source ~/.zshrc` / `source ~/.bashrc`) and re-run `threadnote install`."
|
|
10182
|
+
);
|
|
10183
|
+
return false;
|
|
10184
|
+
}
|
|
10185
|
+
console.warn(`uv install script failed: ${(result.stderr || result.stdout).trim()}`);
|
|
10186
|
+
}
|
|
10187
|
+
console.warn(
|
|
10188
|
+
"Could not install uv automatically. Install it manually (brew install uv) and re-run threadnote install."
|
|
10189
|
+
);
|
|
10190
|
+
return false;
|
|
10191
|
+
}
|
|
9356
10192
|
async function getPythonSystemCertificatesInstallCommand(serverPath) {
|
|
9357
10193
|
const pythonPath = await siblingPythonForExecutable(serverPath);
|
|
9358
10194
|
if (!pythonPath) {
|
|
@@ -9421,38 +10257,38 @@ function printInstallNextSteps(options) {
|
|
|
9421
10257
|
}
|
|
9422
10258
|
async function writeTemplateIfMissing(options) {
|
|
9423
10259
|
if (await exists(options.destinationPath)) {
|
|
9424
|
-
const currentContent = await (0,
|
|
10260
|
+
const currentContent = await (0, import_promises9.readFile)(options.destinationPath, "utf8");
|
|
9425
10261
|
if (options.shouldRepair?.(currentContent) !== true) {
|
|
9426
10262
|
console.log(`Already exists: ${options.destinationPath}`);
|
|
9427
10263
|
return;
|
|
9428
10264
|
}
|
|
9429
|
-
const rendered2 = renderTemplate(await (0,
|
|
10265
|
+
const rendered2 = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
|
|
9430
10266
|
if (options.dryRun) {
|
|
9431
10267
|
console.log(`Would repair generated config: ${options.destinationPath}`);
|
|
9432
10268
|
return;
|
|
9433
10269
|
}
|
|
9434
10270
|
const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
|
|
9435
|
-
await (0,
|
|
9436
|
-
await (0,
|
|
9437
|
-
await (0,
|
|
9438
|
-
await (0,
|
|
10271
|
+
await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
|
|
10272
|
+
await (0, import_promises9.chmod)(backupPath, 384);
|
|
10273
|
+
await (0, import_promises9.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
|
|
10274
|
+
await (0, import_promises9.chmod)(options.destinationPath, 384);
|
|
9439
10275
|
console.log(`Repaired generated config: ${options.destinationPath}`);
|
|
9440
10276
|
console.log(`Backup: ${backupPath}`);
|
|
9441
10277
|
return;
|
|
9442
10278
|
}
|
|
9443
|
-
const rendered = renderTemplate(await (0,
|
|
10279
|
+
const rendered = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
|
|
9444
10280
|
if (options.dryRun) {
|
|
9445
10281
|
console.log(`Would write ${options.destinationPath}`);
|
|
9446
10282
|
return;
|
|
9447
10283
|
}
|
|
9448
|
-
await ensureDirectory((0,
|
|
9449
|
-
await (0,
|
|
9450
|
-
await (0,
|
|
10284
|
+
await ensureDirectory((0, import_node_path8.dirname)(options.destinationPath), false);
|
|
10285
|
+
await (0, import_promises9.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
|
|
10286
|
+
await (0, import_promises9.chmod)(options.destinationPath, 384);
|
|
9451
10287
|
console.log(`Wrote ${options.destinationPath}`);
|
|
9452
10288
|
}
|
|
9453
10289
|
async function installCommandShim(dryRun) {
|
|
9454
10290
|
const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
|
|
9455
|
-
const shimPath = (0,
|
|
10291
|
+
const shimPath = (0, import_node_path8.join)(binDir, "threadnote");
|
|
9456
10292
|
const existingContent = await readFileIfExists(shimPath);
|
|
9457
10293
|
if (existingContent && !isManagedCommandShim(existingContent)) {
|
|
9458
10294
|
console.log(`WARN not overwriting existing command shim: ${shimPath}`);
|
|
@@ -9468,12 +10304,12 @@ async function installCommandShim(dryRun) {
|
|
|
9468
10304
|
return;
|
|
9469
10305
|
}
|
|
9470
10306
|
await ensureDirectory(binDir, false);
|
|
9471
|
-
await (0,
|
|
9472
|
-
await (0,
|
|
10307
|
+
await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
|
|
10308
|
+
await (0, import_promises9.chmod)(shimPath, 493);
|
|
9473
10309
|
console.log(`Wrote command shim: ${shimPath}`);
|
|
9474
10310
|
}
|
|
9475
10311
|
async function removeCommandShim(dryRun) {
|
|
9476
|
-
const shimPath = (0,
|
|
10312
|
+
const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
9477
10313
|
const content = await readFileIfExists(shimPath);
|
|
9478
10314
|
if (content === void 0) {
|
|
9479
10315
|
console.log(`Already absent: ${shimPath}`);
|
|
@@ -9507,8 +10343,8 @@ async function installUserAgentInstructions(dryRun) {
|
|
|
9507
10343
|
console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
|
|
9508
10344
|
continue;
|
|
9509
10345
|
}
|
|
9510
|
-
await ensureDirectory((0,
|
|
9511
|
-
await (0,
|
|
10346
|
+
await ensureDirectory((0, import_node_path8.dirname)(targetPath), false);
|
|
10347
|
+
await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
9512
10348
|
console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
|
|
9513
10349
|
}
|
|
9514
10350
|
}
|
|
@@ -9545,7 +10381,7 @@ async function removeUserAgentInstructions(dryRun) {
|
|
|
9545
10381
|
console.log(`Would update ${targetPath}`);
|
|
9546
10382
|
continue;
|
|
9547
10383
|
}
|
|
9548
|
-
await (0,
|
|
10384
|
+
await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
9549
10385
|
console.log(`Updated ${targetPath}`);
|
|
9550
10386
|
}
|
|
9551
10387
|
}
|
|
@@ -9565,7 +10401,7 @@ async function renderUserAgentInstructions(target) {
|
|
|
9565
10401
|
].join("\n");
|
|
9566
10402
|
}
|
|
9567
10403
|
async function renderUserAgentInstructionsBlock() {
|
|
9568
|
-
const instructions = (await (0,
|
|
10404
|
+
const instructions = (await (0, import_promises9.readFile)((0, import_node_path8.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
|
|
9569
10405
|
return `${USER_INSTRUCTIONS_START_MARKER}
|
|
9570
10406
|
${instructions}
|
|
9571
10407
|
${USER_INSTRUCTIONS_END_MARKER}`;
|
|
@@ -9640,12 +10476,12 @@ function isManagedCommandShim(content) {
|
|
|
9640
10476
|
return content.includes(SHIM_MARKER);
|
|
9641
10477
|
}
|
|
9642
10478
|
async function installLaunchAgent(config, dryRun) {
|
|
9643
|
-
if ((0,
|
|
10479
|
+
if ((0, import_node_os6.platform)() !== "darwin") {
|
|
9644
10480
|
throw new Error("launchd autostart is only supported on macOS.");
|
|
9645
10481
|
}
|
|
9646
|
-
const source = (0,
|
|
10482
|
+
const source = (0, import_node_path8.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
|
|
9647
10483
|
const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
9648
|
-
const rendered = renderTemplate(await (0,
|
|
10484
|
+
const rendered = renderTemplate(await (0, import_promises9.readFile)(source, "utf8"), config);
|
|
9649
10485
|
if (dryRun) {
|
|
9650
10486
|
console.log(`Would write ${destination}`);
|
|
9651
10487
|
console.log(`Would run: launchctl unload ${destination}`);
|
|
@@ -9653,9 +10489,9 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
9653
10489
|
console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
|
|
9654
10490
|
return;
|
|
9655
10491
|
}
|
|
9656
|
-
await ensureDirectory((0,
|
|
9657
|
-
await ensureDirectory((0,
|
|
9658
|
-
await (0,
|
|
10492
|
+
await ensureDirectory((0, import_node_path8.dirname)(destination), false);
|
|
10493
|
+
await ensureDirectory((0, import_node_path8.dirname)(openVikingLogPath(config)), false);
|
|
10494
|
+
await (0, import_promises9.writeFile)(destination, rendered, "utf8");
|
|
9659
10495
|
await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
|
|
9660
10496
|
await maybeRun(false, "launchctl", ["load", destination]);
|
|
9661
10497
|
await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
|
|
@@ -9718,7 +10554,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
|
|
|
9718
10554
|
if (typeof parsed.default_user !== "string") {
|
|
9719
10555
|
return false;
|
|
9720
10556
|
}
|
|
9721
|
-
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0,
|
|
10557
|
+
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path8.join)(config.agentContextHome, "data")) {
|
|
9722
10558
|
return false;
|
|
9723
10559
|
}
|
|
9724
10560
|
return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
|
|
@@ -9825,6 +10661,31 @@ async function main() {
|
|
|
9825
10661
|
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
10662
|
await runForget(getRuntimeConfig(program2), uri, options);
|
|
9827
10663
|
});
|
|
10664
|
+
const share = program2.command("share").description("Share durable memories with teammates through a git-backed repository");
|
|
10665
|
+
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(
|
|
10666
|
+
"--no-push",
|
|
10667
|
+
"Do not push the auto-generated .gitignore housekeeping commit. Does not affect future publishes."
|
|
10668
|
+
).option("--dry-run", "Print actions without running them").action(async (remoteUrl, options) => {
|
|
10669
|
+
await runShareInit(getRuntimeConfig(program2), remoteUrl, options);
|
|
10670
|
+
});
|
|
10671
|
+
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) => {
|
|
10672
|
+
await runShareStatus(getRuntimeConfig(program2), options);
|
|
10673
|
+
});
|
|
10674
|
+
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) => {
|
|
10675
|
+
await runShareSync(getRuntimeConfig(program2), options);
|
|
10676
|
+
});
|
|
10677
|
+
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) => {
|
|
10678
|
+
await runSharePublish(getRuntimeConfig(program2), uri, options);
|
|
10679
|
+
});
|
|
10680
|
+
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) => {
|
|
10681
|
+
await runShareUnpublish(getRuntimeConfig(program2), uri, options);
|
|
10682
|
+
});
|
|
10683
|
+
share.command("list").description("List configured shared teams").option("--dry-run", "Print without side effects (no-op for list)").action(async (options) => {
|
|
10684
|
+
await runShareList(getRuntimeConfig(program2), options);
|
|
10685
|
+
});
|
|
10686
|
+
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) => {
|
|
10687
|
+
await runShareRemove(getRuntimeConfig(program2), options);
|
|
10688
|
+
});
|
|
9828
10689
|
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
10690
|
await runExportPack(getRuntimeConfig(program2), options);
|
|
9830
10691
|
});
|