threadnote 0.6.2 → 0.7.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 +7 -3
- package/dist/mcp_server.cjs +424 -27
- package/dist/threadnote.cjs +2387 -2078
- package/docs/agent-instructions.md +3 -2
- package/docs/index.html +10 -8
- package/docs/share.md +15 -2
- package/package.json +1 -1
package/dist/threadnote.cjs
CHANGED
|
@@ -3488,6 +3488,7 @@ var SHIM_MARKER = "Generated by threadnote";
|
|
|
3488
3488
|
var USER_INSTRUCTIONS_START_MARKER = "<!-- BEGIN THREADNOTE USER INSTRUCTIONS -->";
|
|
3489
3489
|
var USER_INSTRUCTIONS_END_MARKER = "<!-- END THREADNOTE USER INSTRUCTIONS -->";
|
|
3490
3490
|
var USER_MANIFEST_NAME = "seed-manifest.yaml";
|
|
3491
|
+
var SEED_STATE_FILE = "seed-state.json";
|
|
3491
3492
|
var USER_AGENT_INSTRUCTION_TARGETS = [
|
|
3492
3493
|
{ kind: "block", label: "codex user instructions", path: "~/.codex/AGENTS.md" },
|
|
3493
3494
|
{ kind: "block", label: "claude user instructions", path: "~/.claude/CLAUDE.md" },
|
|
@@ -3522,8 +3523,8 @@ var DEFAULT_SEED_PATTERNS = [
|
|
|
3522
3523
|
];
|
|
3523
3524
|
|
|
3524
3525
|
// src/hooks.ts
|
|
3525
|
-
var
|
|
3526
|
-
var
|
|
3526
|
+
var import_promises7 = require("node:fs/promises");
|
|
3527
|
+
var import_node_path8 = require("node:path");
|
|
3527
3528
|
|
|
3528
3529
|
// src/mcp.ts
|
|
3529
3530
|
var import_node_fs2 = require("node:fs");
|
|
@@ -4256,8 +4257,11 @@ function buildCopilotMcpServerConfig(config, options) {
|
|
|
4256
4257
|
type: "stdio"
|
|
4257
4258
|
};
|
|
4258
4259
|
}
|
|
4260
|
+
function isEmptyConfigContent(content) {
|
|
4261
|
+
return content === void 0 || content.trim().length === 0;
|
|
4262
|
+
}
|
|
4259
4263
|
function renderCursorMcpConfig(currentContent, name, serverConfig) {
|
|
4260
|
-
const parsed = currentContent
|
|
4264
|
+
const parsed = isEmptyConfigContent(currentContent) ? {} : parseJsonConfigObject(currentContent ?? "");
|
|
4261
4265
|
if (parsed === void 0) {
|
|
4262
4266
|
throw new Error(`${cursorMcpConfigPath()} exists but is not a JSON object; not modifying it.`);
|
|
4263
4267
|
}
|
|
@@ -4272,7 +4276,7 @@ function renderCursorMcpConfig(currentContent, name, serverConfig) {
|
|
|
4272
4276
|
`;
|
|
4273
4277
|
}
|
|
4274
4278
|
function renderCopilotMcpConfig(currentContent, name, serverConfig) {
|
|
4275
|
-
const parsed = currentContent
|
|
4279
|
+
const parsed = isEmptyConfigContent(currentContent) ? {} : parseJsonConfigObject(currentContent ?? "");
|
|
4276
4280
|
if (parsed === void 0) {
|
|
4277
4281
|
throw new Error(`${copilotMcpConfigPath()} exists but is not a JSON object; not modifying it.`);
|
|
4278
4282
|
}
|
|
@@ -4289,11 +4293,11 @@ function renderCopilotMcpConfig(currentContent, name, serverConfig) {
|
|
|
4289
4293
|
async function removeCursorMcpConfig(name, dryRun) {
|
|
4290
4294
|
const path = cursorMcpConfigPath();
|
|
4291
4295
|
const currentContent = await readFileIfExists(path);
|
|
4292
|
-
if (currentContent
|
|
4296
|
+
if (isEmptyConfigContent(currentContent)) {
|
|
4293
4297
|
console.log(`Already absent: ${path}`);
|
|
4294
4298
|
return;
|
|
4295
4299
|
}
|
|
4296
|
-
const parsed = parseJsonConfigObject(currentContent);
|
|
4300
|
+
const parsed = parseJsonConfigObject(currentContent ?? "");
|
|
4297
4301
|
if (parsed === void 0) {
|
|
4298
4302
|
console.log(`WARN ${path} exists but is not a JSON object; not modifying it.`);
|
|
4299
4303
|
return;
|
|
@@ -4318,11 +4322,11 @@ async function removeCursorMcpConfig(name, dryRun) {
|
|
|
4318
4322
|
async function removeCopilotMcpConfig(name, dryRun) {
|
|
4319
4323
|
const path = copilotMcpConfigPath();
|
|
4320
4324
|
const currentContent = await readFileIfExists(path);
|
|
4321
|
-
if (currentContent
|
|
4325
|
+
if (isEmptyConfigContent(currentContent)) {
|
|
4322
4326
|
console.log(`Already absent: ${path}`);
|
|
4323
4327
|
return;
|
|
4324
4328
|
}
|
|
4325
|
-
const parsed = parseJsonConfigObject(currentContent);
|
|
4329
|
+
const parsed = parseJsonConfigObject(currentContent ?? "");
|
|
4326
4330
|
if (parsed === void 0) {
|
|
4327
4331
|
console.log(`WARN ${path} exists but is not a JSON object; not modifying it.`);
|
|
4328
4332
|
return;
|
|
@@ -4483,8 +4487,8 @@ function existsSyncDirectory(path) {
|
|
|
4483
4487
|
}
|
|
4484
4488
|
|
|
4485
4489
|
// src/memory.ts
|
|
4486
|
-
var
|
|
4487
|
-
var
|
|
4490
|
+
var import_promises5 = require("node:fs/promises");
|
|
4491
|
+
var import_node_path5 = require("node:path");
|
|
4488
4492
|
|
|
4489
4493
|
// src/manifest.ts
|
|
4490
4494
|
var import_promises3 = require("node:fs/promises");
|
|
@@ -7226,2398 +7230,2698 @@ function renderTemplate(template, config) {
|
|
|
7226
7230
|
return template.replaceAll("{{THREADNOTE_HOME}}", config.agentContextHome).replaceAll("{{OPENVIKING_ACCOUNT}}", config.account).replaceAll("{{OPENVIKING_AGENT_ID}}", config.agentId).replaceAll("{{OPENVIKING_HOST}}", config.host).replaceAll("{{OPENVIKING_PORT}}", String(config.port)).replaceAll("{{OPENVIKING_USER}}", config.user);
|
|
7227
7231
|
}
|
|
7228
7232
|
|
|
7229
|
-
// src/
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
+
// src/share.ts
|
|
7234
|
+
var import_promises4 = require("node:fs/promises");
|
|
7235
|
+
var import_node_os4 = require("node:os");
|
|
7236
|
+
var import_node_path4 = require("node:path");
|
|
7237
|
+
var TEAMS_FILE_VERSION = 1;
|
|
7238
|
+
var SHARED_SEGMENT = "shared";
|
|
7239
|
+
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
7240
|
+
var AUTO_SHARE_FETCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
7241
|
+
var DEFAULT_GIT_REMOTE_NAME = "origin";
|
|
7242
|
+
var SCRUBBER_PATTERNS = [
|
|
7243
|
+
// Credentials: never redactable. Blocking is the only safe response —
|
|
7244
|
+
// automated redaction risks false negatives that leave material in git.
|
|
7245
|
+
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
7246
|
+
{ name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
7247
|
+
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
7248
|
+
{ name: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}/ },
|
|
7249
|
+
{ name: "GitLab PAT", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
|
|
7250
|
+
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
7251
|
+
// Matches bare JWTs (three base64url segments). May surface a JWE token in
|
|
7252
|
+
// legitimate docs; if that becomes noisy we can switch to warn-only.
|
|
7253
|
+
{ name: "JWT", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
7254
|
+
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
7255
|
+
// Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
|
|
7256
|
+
// xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
|
|
7257
|
+
{ name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i },
|
|
7258
|
+
// Soft leaks: block by default (so the agent sees them and decides), but
|
|
7259
|
+
// allow opt-in redaction so curated memories with incidental matches can
|
|
7260
|
+
// ship without a manual rewrite. Local home paths are the recurring
|
|
7261
|
+
// real-world leak; the regexes greedily consume the whole path segment
|
|
7262
|
+
// (including subdirectories) up to whitespace or common closing punctuation
|
|
7263
|
+
// so redaction collapses an entire path to a single placeholder rather than
|
|
7264
|
+
// leaving the subpath visible.
|
|
7265
|
+
{ name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
|
|
7266
|
+
{ name: "linux home path", placeholder: "<local-path>", regex: /\b\/home\/[^\s)>"'`,]+/ }
|
|
7267
|
+
];
|
|
7268
|
+
function applyScrubber(content, { redact }) {
|
|
7269
|
+
let cleaned = content;
|
|
7270
|
+
const redactions = [];
|
|
7271
|
+
for (const pattern of SCRUBBER_PATTERNS) {
|
|
7272
|
+
if (!pattern.regex.test(cleaned)) {
|
|
7273
|
+
continue;
|
|
7274
|
+
}
|
|
7275
|
+
if (!pattern.placeholder || !redact) {
|
|
7276
|
+
return { blocker: pattern.name, cleaned: content, redactions: [] };
|
|
7277
|
+
}
|
|
7278
|
+
const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`;
|
|
7279
|
+
const globalRegex = new RegExp(pattern.regex.source, flags);
|
|
7280
|
+
const matches = cleaned.match(globalRegex) ?? [];
|
|
7281
|
+
cleaned = cleaned.replace(globalRegex, pattern.placeholder);
|
|
7282
|
+
redactions.push({ count: matches.length, name: pattern.name });
|
|
7233
7283
|
}
|
|
7234
|
-
|
|
7284
|
+
return { cleaned, redactions };
|
|
7235
7285
|
}
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7286
|
+
var autoShareStates = /* @__PURE__ */ new Map();
|
|
7287
|
+
async function runShareInit(config, remoteUrl, options) {
|
|
7288
|
+
if (!remoteUrl.trim()) {
|
|
7289
|
+
throw new Error("Provide a git remote URL for the shared memories repo.");
|
|
7239
7290
|
}
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7291
|
+
const dryRun = options.dryRun === true;
|
|
7292
|
+
const teamName = normalizeTeamName(options.team);
|
|
7293
|
+
const teamsFile = await readTeamsFile(config);
|
|
7294
|
+
if (teamsFile.teams[teamName]) {
|
|
7295
|
+
throw new Error(
|
|
7296
|
+
`Team "${teamName}" is already configured (remote ${teamsFile.teams[teamName].remote}). Remove it first with: threadnote share remove --team ${teamName}`
|
|
7297
|
+
);
|
|
7246
7298
|
}
|
|
7247
|
-
const
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7299
|
+
const worktree = teamWorktreePath(config, teamName);
|
|
7300
|
+
const gitdir = teamGitdirPath(config, teamName);
|
|
7301
|
+
await assertWorktreeUsable(worktree);
|
|
7302
|
+
if (await exists(gitdir)) {
|
|
7303
|
+
throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
|
|
7304
|
+
}
|
|
7305
|
+
await ensureDirectory((0, import_node_path4.dirname)(worktree), dryRun);
|
|
7306
|
+
await ensureDirectory((0, import_node_path4.dirname)(gitdir), dryRun);
|
|
7307
|
+
const git = await requiredExecutable("git");
|
|
7308
|
+
await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
|
|
7309
|
+
const newConfig = {
|
|
7310
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7311
|
+
gitdir,
|
|
7312
|
+
name: teamName,
|
|
7313
|
+
remote: remoteUrl,
|
|
7314
|
+
worktree
|
|
7254
7315
|
};
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7316
|
+
const updatedTeams = {
|
|
7317
|
+
defaultTeam: shouldSetDefault(options, teamsFile) ? teamName : teamsFile.defaultTeam ?? teamName,
|
|
7318
|
+
teams: { ...teamsFile.teams, [teamName]: newConfig },
|
|
7319
|
+
version: TEAMS_FILE_VERSION
|
|
7320
|
+
};
|
|
7321
|
+
if (dryRun) {
|
|
7322
|
+
console.log(`Would write teams file: ${teamsFilePath(config)}`);
|
|
7323
|
+
console.log(`Would set ${teamName} as default? ${updatedTeams.defaultTeam === teamName}`);
|
|
7324
|
+
} else {
|
|
7325
|
+
await writeTeamsFile(config, updatedTeams);
|
|
7326
|
+
console.log(`Configured shared team "${teamName}" -> ${portablePath(worktree)}`);
|
|
7327
|
+
}
|
|
7328
|
+
if (!dryRun) {
|
|
7329
|
+
await ensureSharedGitignore(worktree, git, options.push !== false);
|
|
7330
|
+
const ingested = await ingestWorktreeFiles(config, newConfig, "create");
|
|
7331
|
+
console.log(`Ingested ${ingested} shared memory file(s) into OpenViking.`);
|
|
7332
|
+
}
|
|
7262
7333
|
}
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
const
|
|
7267
|
-
|
|
7268
|
-
|
|
7334
|
+
var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
|
|
7335
|
+
var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
|
|
7336
|
+
async function ensureSharedGitignore(worktree, git, push) {
|
|
7337
|
+
const gitignorePath = (0, import_node_path4.join)(worktree, ".gitignore");
|
|
7338
|
+
const existing = await readFileIfExists(gitignorePath) ?? "";
|
|
7339
|
+
const lines = existing.split("\n").map((line) => line.trim());
|
|
7340
|
+
const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
|
|
7341
|
+
if (missingPatterns.length === 0) {
|
|
7269
7342
|
return;
|
|
7270
7343
|
}
|
|
7271
|
-
const
|
|
7272
|
-
const
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
let duplicateCount = 0;
|
|
7276
|
-
let migratedCount = 0;
|
|
7277
|
-
let sensitiveCount = 0;
|
|
7278
|
-
if (!dryRun && candidates.length > 0) {
|
|
7279
|
-
await ensureDurableMemoryDirectory(ov, config);
|
|
7344
|
+
const hasHeader = lines.includes(SHARED_GITIGNORE_HEADER);
|
|
7345
|
+
const segments = [];
|
|
7346
|
+
if (existing.length > 0 && !existing.endsWith("\n")) {
|
|
7347
|
+
segments.push("\n");
|
|
7280
7348
|
}
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7349
|
+
if (existing.length > 0) {
|
|
7350
|
+
segments.push("\n");
|
|
7351
|
+
}
|
|
7352
|
+
if (!hasHeader) {
|
|
7353
|
+
segments.push(SHARED_GITIGNORE_HEADER, "\n");
|
|
7354
|
+
}
|
|
7355
|
+
segments.push(missingPatterns.join("\n"), "\n");
|
|
7356
|
+
await (0, import_promises4.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
|
|
7357
|
+
console.log(`Added ${missingPatterns.join(", ")} to ${portablePath(gitignorePath)}`);
|
|
7358
|
+
await maybeRun(false, git, ["-C", worktree, "add", ".gitignore"]);
|
|
7359
|
+
const commitResult = await runCommand(
|
|
7360
|
+
git,
|
|
7361
|
+
["-C", worktree, "commit", "-m", "share: ignore OpenViking directory summaries"],
|
|
7362
|
+
{ allowFailure: true }
|
|
7363
|
+
);
|
|
7364
|
+
if (commitResult.exitCode !== 0) {
|
|
7365
|
+
const detail = commitResult.stderr.trim() || commitResult.stdout.trim();
|
|
7366
|
+
if (!/nothing to commit|no changes added/i.test(detail)) {
|
|
7367
|
+
console.warn(
|
|
7368
|
+
`.gitignore housekeeping commit was rejected (${detail || "unknown"}); it will be retried on the next share sync.`
|
|
7369
|
+
);
|
|
7370
|
+
return;
|
|
7371
|
+
}
|
|
7372
|
+
}
|
|
7373
|
+
if (push) {
|
|
7374
|
+
await maybeRun(false, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
7375
|
+
}
|
|
7376
|
+
}
|
|
7377
|
+
async function runShareStatus(config, options) {
|
|
7378
|
+
const team = await resolveTeam(config, options.team);
|
|
7379
|
+
const git = await requiredExecutable("git");
|
|
7380
|
+
console.log(`Team: ${team.name}`);
|
|
7381
|
+
console.log(`Remote: ${team.config.remote}`);
|
|
7382
|
+
console.log(`Worktree: ${portablePath(team.config.worktree)}`);
|
|
7383
|
+
console.log(`Gitdir: ${portablePath(team.config.gitdir)}`);
|
|
7384
|
+
await maybeRun(options.dryRun === true, git, ["-C", team.config.worktree, "status", "--short", "--branch"]);
|
|
7385
|
+
await maybeRun(options.dryRun === true, git, ["-C", team.config.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME], {
|
|
7386
|
+
allowFailure: true
|
|
7387
|
+
});
|
|
7388
|
+
const ahead = await gitOutput(team.config.worktree, ["rev-list", "--count", "@{u}..HEAD"], options.dryRun === true);
|
|
7389
|
+
const behind = await gitOutput(team.config.worktree, ["rev-list", "--count", "HEAD..@{u}"], options.dryRun === true);
|
|
7390
|
+
if (ahead !== void 0) {
|
|
7391
|
+
console.log(`Ahead of upstream: ${ahead}`);
|
|
7392
|
+
}
|
|
7393
|
+
if (behind !== void 0) {
|
|
7394
|
+
console.log(`Behind upstream: ${behind}`);
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
async function syncSharedReposBeforeAgentRead(config) {
|
|
7398
|
+
const state = autoShareState(config);
|
|
7399
|
+
return enqueueShareOperation(state, async () => {
|
|
7400
|
+
await loadPendingReindexes(config, state);
|
|
7401
|
+
const warnings = await refreshShareUpdateStateLocked(config, state, { force: false });
|
|
7402
|
+
const syncTeams = /* @__PURE__ */ new Set([...state.behindTeams, ...state.pendingReindexes.keys()]);
|
|
7403
|
+
if (syncTeams.size === 0) {
|
|
7404
|
+
return { syncedTeams: [], warnings };
|
|
7405
|
+
}
|
|
7406
|
+
const syncedTeams = [];
|
|
7407
|
+
const remainingBehind = new Set(state.behindTeams);
|
|
7408
|
+
for (const team of syncTeams) {
|
|
7409
|
+
try {
|
|
7410
|
+
const warning = await runShareSyncQuiet(config, state, { team });
|
|
7411
|
+
if (warning) {
|
|
7412
|
+
warnings.push(warning);
|
|
7413
|
+
} else {
|
|
7414
|
+
remainingBehind.delete(team);
|
|
7415
|
+
syncedTeams.push(team);
|
|
7416
|
+
}
|
|
7417
|
+
} catch (err) {
|
|
7418
|
+
warnings.push(
|
|
7419
|
+
`Auto-sync for shared team "${team}" failed: ${err instanceof Error ? err.message : String(err)}`
|
|
7296
7420
|
);
|
|
7297
|
-
continue;
|
|
7298
|
-
}
|
|
7299
|
-
if (limit !== void 0 && migratedCount >= limit) {
|
|
7300
|
-
break;
|
|
7301
7421
|
}
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7422
|
+
}
|
|
7423
|
+
state.behindTeams = remainingBehind;
|
|
7424
|
+
state.lastCheckedAt = Date.now();
|
|
7425
|
+
return { syncedTeams, warnings };
|
|
7426
|
+
});
|
|
7427
|
+
}
|
|
7428
|
+
function autoShareState(config) {
|
|
7429
|
+
const key = `${config.agentContextHome}:${config.account}:${config.user}`;
|
|
7430
|
+
let state = autoShareStates.get(key);
|
|
7431
|
+
if (!state) {
|
|
7432
|
+
state = { behindTeams: /* @__PURE__ */ new Set(), lastCheckedAt: 0, pendingReindexes: /* @__PURE__ */ new Map() };
|
|
7433
|
+
autoShareStates.set(key, state);
|
|
7434
|
+
}
|
|
7435
|
+
return state;
|
|
7436
|
+
}
|
|
7437
|
+
function pendingReindexesPath(config) {
|
|
7438
|
+
return (0, import_node_path4.join)(config.agentContextHome, "share", "auto-sync-pending-reindexes.json");
|
|
7439
|
+
}
|
|
7440
|
+
async function loadPendingReindexes(config, state) {
|
|
7441
|
+
const raw = await readFileIfExists(pendingReindexesPath(config));
|
|
7442
|
+
if (!raw) {
|
|
7443
|
+
state.pendingReindexes = /* @__PURE__ */ new Map();
|
|
7444
|
+
return;
|
|
7445
|
+
}
|
|
7446
|
+
const parsed = parseJsonConfigObject(raw);
|
|
7447
|
+
if (!parsed || typeof parsed.teams !== "object" || parsed.teams === null || Array.isArray(parsed.teams)) {
|
|
7448
|
+
state.pendingReindexes = /* @__PURE__ */ new Map();
|
|
7449
|
+
return;
|
|
7450
|
+
}
|
|
7451
|
+
const pending = /* @__PURE__ */ new Map();
|
|
7452
|
+
for (const [team, value] of Object.entries(parsed.teams)) {
|
|
7453
|
+
if (!Array.isArray(value)) {
|
|
7454
|
+
continue;
|
|
7455
|
+
}
|
|
7456
|
+
const changes = [];
|
|
7457
|
+
for (const item of value) {
|
|
7458
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
7306
7459
|
continue;
|
|
7307
7460
|
}
|
|
7308
|
-
|
|
7309
|
-
if (
|
|
7310
|
-
|
|
7311
|
-
await (0, import_promises4.chmod)(migrationPath, 384);
|
|
7312
|
-
await writeDurableMemoryFile(ov, config, memoryUri, migrationPath, "create");
|
|
7313
|
-
existingHashes.add(candidate.hash);
|
|
7461
|
+
const entry = item;
|
|
7462
|
+
if (typeof entry.path === "string" && typeof entry.relativePath === "string" && (entry.status === "added" || entry.status === "removed" || entry.status === "modified")) {
|
|
7463
|
+
changes.push({ path: entry.path, relativePath: entry.relativePath, status: entry.status });
|
|
7314
7464
|
}
|
|
7315
|
-
migratedCount += 1;
|
|
7316
7465
|
}
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
await (0, import_promises4.rm)(migrationPath, { force: true });
|
|
7466
|
+
if (changes.length > 0) {
|
|
7467
|
+
pending.set(team, changes);
|
|
7320
7468
|
}
|
|
7321
7469
|
}
|
|
7322
|
-
|
|
7323
|
-
[
|
|
7324
|
-
`Migration summary: ${migratedCount} ${dryRun ? "would be migrated" : "migrated"}`,
|
|
7325
|
-
`${duplicateCount} duplicate(s) skipped`,
|
|
7326
|
-
`${sensitiveCount} sensitive-looking item(s) skipped`,
|
|
7327
|
-
`${candidates.length} legacy Threadnote item(s) scanned`,
|
|
7328
|
-
`source account(s): ${sourceAccounts.join(", ")}`
|
|
7329
|
-
].join("; ")
|
|
7330
|
-
);
|
|
7470
|
+
state.pendingReindexes = pending;
|
|
7331
7471
|
}
|
|
7332
|
-
async function
|
|
7333
|
-
const
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7472
|
+
async function writePendingReindexes(config, state) {
|
|
7473
|
+
const path = pendingReindexesPath(config);
|
|
7474
|
+
if (state.pendingReindexes.size === 0) {
|
|
7475
|
+
await (0, import_promises4.rm)(path, { force: true });
|
|
7476
|
+
return;
|
|
7477
|
+
}
|
|
7478
|
+
const contents = {
|
|
7479
|
+
teams: Object.fromEntries(state.pendingReindexes),
|
|
7480
|
+
version: 1
|
|
7481
|
+
};
|
|
7482
|
+
await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
7483
|
+
const tempPath = `${path}.${process.pid}.tmp`;
|
|
7484
|
+
await (0, import_promises4.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
|
|
7485
|
+
`, { encoding: "utf8", mode: 384 });
|
|
7486
|
+
await (0, import_promises4.rename)(tempPath, path);
|
|
7487
|
+
}
|
|
7488
|
+
async function refreshShareUpdateStateLocked(config, state, options) {
|
|
7489
|
+
const now = Date.now();
|
|
7490
|
+
if (!options.force && state.lastCheckedAt > 0 && now - state.lastCheckedAt < AUTO_SHARE_FETCH_INTERVAL_MS) {
|
|
7491
|
+
return [];
|
|
7492
|
+
}
|
|
7341
7493
|
try {
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7494
|
+
const statuses = await fetchShareUpdateStatuses(config);
|
|
7495
|
+
const nextBehindTeams = new Set(state.behindTeams);
|
|
7496
|
+
for (const status of statuses) {
|
|
7497
|
+
if (status.warning) {
|
|
7498
|
+
continue;
|
|
7345
7499
|
}
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
["Migrated legacy handoff from the historical events trail.", "", candidate.original.trim()].join("\n")
|
|
7351
|
-
);
|
|
7352
|
-
console.log(`${dryRun ? "Would migrate" : "Migrating"} ${candidate.sourceUri} -> ${destinationUri}`);
|
|
7353
|
-
if (!dryRun) {
|
|
7354
|
-
if (await vikingResourceExists(ov, config, destinationUri)) {
|
|
7355
|
-
existingCount += 1;
|
|
7356
|
-
console.log(`Archived copy already exists; cleaning up legacy source: ${candidate.sourceUri}`);
|
|
7357
|
-
} else {
|
|
7358
|
-
await (0, import_promises4.writeFile)(migrationPath, migratedMemory, { encoding: "utf8", mode: 384 });
|
|
7359
|
-
await (0, import_promises4.chmod)(migrationPath, 384);
|
|
7360
|
-
await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, candidate.metadata));
|
|
7361
|
-
await writeDurableMemoryFile(ov, config, destinationUri, migrationPath, "create");
|
|
7362
|
-
}
|
|
7363
|
-
const removedOriginal = await removeVikingResourceWithRetry(ov, config, candidate.sourceUri);
|
|
7364
|
-
if (!removedOriginal) {
|
|
7365
|
-
console.error(
|
|
7366
|
-
`Migrated copy stored, but original is still processing. Retry later: threadnote forget ${candidate.sourceUri}`
|
|
7367
|
-
);
|
|
7368
|
-
skippedCount += 1;
|
|
7369
|
-
}
|
|
7500
|
+
if (status.behind > 0) {
|
|
7501
|
+
nextBehindTeams.add(status.team);
|
|
7502
|
+
} else {
|
|
7503
|
+
nextBehindTeams.delete(status.team);
|
|
7370
7504
|
}
|
|
7371
|
-
migratedCount += 1;
|
|
7372
7505
|
}
|
|
7506
|
+
state.behindTeams = nextBehindTeams;
|
|
7507
|
+
return statuses.flatMap((status) => status.warning ? [status.warning] : []);
|
|
7373
7508
|
} finally {
|
|
7374
|
-
|
|
7375
|
-
await (0, import_promises4.rm)(migrationPath, { force: true });
|
|
7376
|
-
}
|
|
7509
|
+
state.lastCheckedAt = Date.now();
|
|
7377
7510
|
}
|
|
7378
|
-
console.log(
|
|
7379
|
-
[
|
|
7380
|
-
`Lifecycle migration summary: ${migratedCount} clear legacy handoff(s) ${dryRun ? "would be migrated" : "migrated"}`,
|
|
7381
|
-
`${existingCount} existing archived copy/copies reused`,
|
|
7382
|
-
`${skippedCount} legacy source(s) still processing`,
|
|
7383
|
-
`${candidates.length} clear legacy handoff candidate(s) found`,
|
|
7384
|
-
dryRun ? "Run with --apply to perform this migration." : void 0
|
|
7385
|
-
].filter((part) => part !== void 0).join("; ")
|
|
7386
|
-
);
|
|
7387
7511
|
}
|
|
7388
|
-
|
|
7389
|
-
const
|
|
7390
|
-
const
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
args.push("--uri", inferredUri);
|
|
7394
|
-
console.log(`Recall scope: ${inferredUri}`);
|
|
7395
|
-
}
|
|
7396
|
-
if (options.nodeLimit) {
|
|
7397
|
-
args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
|
|
7398
|
-
}
|
|
7399
|
-
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
7400
|
-
await augmentRecallWithSeededResources(config, ov, options, inferredUri);
|
|
7401
|
-
await printExactMemoryMatches(config, ov, options.query, {
|
|
7402
|
-
dryRun: options.dryRun === true,
|
|
7403
|
-
includeArchived: options.includeArchived === true
|
|
7404
|
-
});
|
|
7512
|
+
function enqueueShareOperation(state, action) {
|
|
7513
|
+
const previous = state.operationPromise ?? Promise.resolve();
|
|
7514
|
+
const current = previous.catch(() => void 0).then(action);
|
|
7515
|
+
state.operationPromise = current.catch(() => void 0);
|
|
7516
|
+
return current;
|
|
7405
7517
|
}
|
|
7406
|
-
async function
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
if (!project) {
|
|
7412
|
-
return;
|
|
7413
|
-
}
|
|
7414
|
-
const projectResourceUri = trimTrailingSlash(project.uri);
|
|
7415
|
-
if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
|
|
7416
|
-
return;
|
|
7518
|
+
async function fetchShareUpdateStatuses(config) {
|
|
7519
|
+
const teamsFile = await readTeamsFile(config);
|
|
7520
|
+
const teams = Object.entries(teamsFile.teams);
|
|
7521
|
+
if (teams.length === 0) {
|
|
7522
|
+
return [];
|
|
7417
7523
|
}
|
|
7418
|
-
const
|
|
7419
|
-
|
|
7420
|
-
|
|
7524
|
+
const git = await requiredExecutable("git");
|
|
7525
|
+
const statuses = [];
|
|
7526
|
+
for (const [name, team] of teams) {
|
|
7527
|
+
const fetchResult = await runCommand(git, ["-C", team.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME], {
|
|
7528
|
+
allowFailure: true
|
|
7529
|
+
});
|
|
7530
|
+
if (fetchResult.exitCode !== 0) {
|
|
7531
|
+
statuses.push({
|
|
7532
|
+
behind: 0,
|
|
7533
|
+
team: name,
|
|
7534
|
+
warning: `Auto-sync check for shared team "${name}" failed: ${fetchResult.stderr.trim() || fetchResult.stdout.trim() || "unknown git fetch error"}`
|
|
7535
|
+
});
|
|
7536
|
+
continue;
|
|
7537
|
+
}
|
|
7538
|
+
const behind = await gitOutput(team.worktree, ["rev-list", "--count", "HEAD..@{u}"], false);
|
|
7539
|
+
if (behind === void 0) {
|
|
7540
|
+
statuses.push({
|
|
7541
|
+
behind: 0,
|
|
7542
|
+
team: name,
|
|
7543
|
+
warning: `Auto-sync check for shared team "${name}" failed: could not read upstream behind count.`
|
|
7544
|
+
});
|
|
7545
|
+
continue;
|
|
7546
|
+
}
|
|
7547
|
+
statuses.push({ behind: Number.parseInt(behind, 10) || 0, team: name });
|
|
7421
7548
|
}
|
|
7422
|
-
|
|
7423
|
-
Also searching seeded resources: ${projectResourceUri}`);
|
|
7424
|
-
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
7549
|
+
return statuses;
|
|
7425
7550
|
}
|
|
7426
|
-
async function
|
|
7427
|
-
|
|
7428
|
-
const
|
|
7429
|
-
const
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
console.log(` threadnote list ${parentUri2} --all --recursive`);
|
|
7551
|
+
async function runShareSync(config, options) {
|
|
7552
|
+
const team = await resolveTeam(config, options.team);
|
|
7553
|
+
const dryRun = options.dryRun === true;
|
|
7554
|
+
const git = await requiredExecutable("git");
|
|
7555
|
+
const worktree = team.config.worktree;
|
|
7556
|
+
if (!dryRun) {
|
|
7557
|
+
await ensureSharedGitignore(worktree, git, false);
|
|
7434
7558
|
}
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7559
|
+
if (await hasUncommittedChanges(worktree)) {
|
|
7560
|
+
if (options.autoCommit === false) {
|
|
7561
|
+
throw new Error(
|
|
7562
|
+
`Worktree ${worktree} has uncommitted changes. Commit them yourself or rerun without --no-auto-commit.`
|
|
7563
|
+
);
|
|
7564
|
+
}
|
|
7565
|
+
const message = options.message ?? `share: sync ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
7566
|
+
await stageShareableChanges(dryRun, git, worktree);
|
|
7567
|
+
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
7442
7568
|
}
|
|
7443
|
-
|
|
7444
|
-
|
|
7569
|
+
const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], dryRun);
|
|
7570
|
+
await maybeRun(dryRun, git, ["-C", worktree, "fetch", DEFAULT_GIT_REMOTE_NAME]);
|
|
7571
|
+
const pullResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
7572
|
+
if (dryRun) {
|
|
7573
|
+
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
7574
|
+
} else if (pullResult && pullResult.exitCode !== 0) {
|
|
7575
|
+
if (await exists((0, import_node_path4.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path4.join)(team.config.gitdir, "rebase-apply"))) {
|
|
7576
|
+
throw new Error(
|
|
7577
|
+
`git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
|
|
7578
|
+
Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
|
|
7579
|
+
);
|
|
7580
|
+
}
|
|
7581
|
+
throw new Error(
|
|
7582
|
+
`git pull --rebase failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
|
|
7583
|
+
);
|
|
7445
7584
|
}
|
|
7446
|
-
|
|
7447
|
-
|
|
7585
|
+
const afterRev = await gitOutput(worktree, ["rev-parse", "HEAD"], dryRun);
|
|
7586
|
+
if (!dryRun && beforeRev && afterRev && beforeRev !== afterRev) {
|
|
7587
|
+
const changes = await listChangedFiles(worktree, beforeRev, afterRev);
|
|
7588
|
+
await applyChangesToOpenViking(config, team.config, changes);
|
|
7589
|
+
console.log(`Reindexed ${changes.length} file change(s) into OpenViking.`);
|
|
7590
|
+
} else if (!dryRun) {
|
|
7591
|
+
console.log("No upstream changes to reindex.");
|
|
7448
7592
|
}
|
|
7449
|
-
if (options.
|
|
7450
|
-
|
|
7593
|
+
if (options.push !== false) {
|
|
7594
|
+
await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
7451
7595
|
}
|
|
7452
|
-
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
7453
|
-
}
|
|
7454
|
-
async function runHandoff(config, options) {
|
|
7455
|
-
const { bodyText, metadata } = await buildHandoff(options);
|
|
7456
|
-
await storeMemory(config, {
|
|
7457
|
-
bodyText,
|
|
7458
|
-
dryRun: options.dryRun === true,
|
|
7459
|
-
metadata,
|
|
7460
|
-
replaceUri: options.replace,
|
|
7461
|
-
title: "HANDOFF"
|
|
7462
|
-
});
|
|
7463
7596
|
}
|
|
7464
|
-
async function
|
|
7465
|
-
|
|
7466
|
-
const
|
|
7467
|
-
const
|
|
7468
|
-
const
|
|
7469
|
-
if (
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
project: normalizeOptionalMetadata(options.project),
|
|
7474
|
-
sourceAgentClient: "threadnote",
|
|
7475
|
-
status: "archived",
|
|
7476
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7477
|
-
topic: normalizeOptionalMetadata(options.topic)
|
|
7478
|
-
};
|
|
7479
|
-
await storeMemory(config, {
|
|
7480
|
-
bodyText: ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n"),
|
|
7481
|
-
dryRun: true,
|
|
7482
|
-
metadata: fallbackMetadata,
|
|
7483
|
-
title: "MEMORY"
|
|
7484
|
-
});
|
|
7485
|
-
console.log(formatShellCommand(ov, withIdentity(config, ["rm", uri])));
|
|
7486
|
-
return;
|
|
7597
|
+
async function runShareSyncQuiet(config, state, options) {
|
|
7598
|
+
const team = await resolveTeam(config, options.team);
|
|
7599
|
+
const git = await requiredExecutable("git");
|
|
7600
|
+
const worktree = team.config.worktree;
|
|
7601
|
+
const pendingChanges = state.pendingReindexes.get(team.name);
|
|
7602
|
+
if (pendingChanges) {
|
|
7603
|
+
await applyChangesToOpenViking(config, team.config, pendingChanges, { quiet: true });
|
|
7604
|
+
state.pendingReindexes.delete(team.name);
|
|
7605
|
+
await writePendingReindexes(config, state);
|
|
7487
7606
|
}
|
|
7488
|
-
if (
|
|
7489
|
-
|
|
7607
|
+
if (await hasUncommittedChanges(worktree)) {
|
|
7608
|
+
return `Shared team "${team.name}" has uncommitted changes; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or resolve them.`;
|
|
7490
7609
|
}
|
|
7491
|
-
const
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
kind: options.kind ?? inferredMetadata.kind ?? "handoff",
|
|
7495
|
-
project: normalizeOptionalMetadata(options.project) ?? inferredMetadata.project,
|
|
7496
|
-
sourceAgentClient: "threadnote",
|
|
7497
|
-
status: "archived",
|
|
7498
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7499
|
-
topic: normalizeOptionalMetadata(options.topic) ?? inferredMetadata.topic
|
|
7500
|
-
};
|
|
7501
|
-
await storeMemory(config, {
|
|
7502
|
-
bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
|
|
7503
|
-
dryRun: false,
|
|
7504
|
-
metadata,
|
|
7505
|
-
title: "MEMORY"
|
|
7506
|
-
});
|
|
7507
|
-
const removedOriginal = await removeVikingResourceWithRetry(ov, config, uri);
|
|
7508
|
-
if (removedOriginal) {
|
|
7509
|
-
console.log(`Archived original memory: ${uri}`);
|
|
7510
|
-
} else {
|
|
7511
|
-
console.error(`Archive stored, but original memory is still processing. Retry later: threadnote forget ${uri}`);
|
|
7610
|
+
const ahead = await gitOutput(worktree, ["rev-list", "--count", "@{u}..HEAD"], false);
|
|
7611
|
+
if (ahead === void 0) {
|
|
7612
|
+
return `Shared team "${team.name}" upstream status is unknown; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to inspect and resolve it.`;
|
|
7512
7613
|
}
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
assertVikingUri(uri);
|
|
7516
|
-
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
7517
|
-
if (options.dryRun === true) {
|
|
7518
|
-
await maybeRun(true, ov, withIdentity(config, ["rm", uri]));
|
|
7519
|
-
return;
|
|
7614
|
+
if ((Number.parseInt(ahead, 10) || 0) > 0) {
|
|
7615
|
+
return `Shared team "${team.name}" has local commits ahead of upstream; skipped automatic sync. Run \`threadnote share sync --team ${team.name}\` to publish or reconcile them.`;
|
|
7520
7616
|
}
|
|
7521
|
-
const
|
|
7522
|
-
|
|
7523
|
-
|
|
7617
|
+
const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
|
|
7618
|
+
const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
|
|
7619
|
+
if (pullResult.exitCode !== 0) {
|
|
7620
|
+
if (await exists((0, import_node_path4.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path4.join)(team.config.gitdir, "rebase-apply"))) {
|
|
7621
|
+
throw new Error(
|
|
7622
|
+
`Automatic share sync hit git conflicts in ${worktree}. Resolve them in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then rerun recall/read.`
|
|
7623
|
+
);
|
|
7624
|
+
}
|
|
7625
|
+
throw new Error(
|
|
7626
|
+
`Automatic share sync failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
|
|
7627
|
+
);
|
|
7628
|
+
}
|
|
7629
|
+
const afterRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
|
|
7630
|
+
if (beforeRev && afterRev && beforeRev !== afterRev) {
|
|
7631
|
+
const changes = await listChangedFiles(worktree, beforeRev, afterRev);
|
|
7632
|
+
if (changes.length > 0) {
|
|
7633
|
+
state.pendingReindexes.set(team.name, changes);
|
|
7634
|
+
await writePendingReindexes(config, state);
|
|
7635
|
+
await applyChangesToOpenViking(config, team.config, changes, { quiet: true });
|
|
7636
|
+
state.pendingReindexes.delete(team.name);
|
|
7637
|
+
await writePendingReindexes(config, state);
|
|
7638
|
+
}
|
|
7524
7639
|
}
|
|
7640
|
+
return void 0;
|
|
7525
7641
|
}
|
|
7526
|
-
async function
|
|
7527
|
-
const
|
|
7528
|
-
|
|
7529
|
-
const outputPath = expandPath(options.path ?? defaultPath);
|
|
7530
|
-
await maybeRun(options.dryRun === true, ov, withIdentity(config, ["export", options.uri ?? "viking://", outputPath]));
|
|
7642
|
+
async function stageShareableChanges(dryRun, git, worktree) {
|
|
7643
|
+
const pathspecs = [":(top)README.md", ":(top).gitignore", ...SHAREABLE_MEMORY_KIND_DIRS.map((dir) => `:(top)${dir}`)];
|
|
7644
|
+
await maybeRun(dryRun, git, ["-C", worktree, "add", "--", ...pathspecs], { allowFailure: true });
|
|
7531
7645
|
}
|
|
7532
|
-
async function
|
|
7533
|
-
|
|
7534
|
-
|
|
7646
|
+
async function runSharePublish(config, sourceUri, options) {
|
|
7647
|
+
assertVikingUri(sourceUri);
|
|
7648
|
+
const team = await resolveTeam(config, options.team);
|
|
7649
|
+
const dryRun = options.dryRun === true;
|
|
7650
|
+
const preview = options.preview === true;
|
|
7651
|
+
if (isInSharedNamespace(config, sourceUri)) {
|
|
7652
|
+
throw new Error(`Memory ${sourceUri} is already in the shared namespace.`);
|
|
7535
7653
|
}
|
|
7536
|
-
const ov = await openVikingCliForMode(
|
|
7537
|
-
await
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
)
|
|
7542
|
-
}
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
}
|
|
7550
|
-
async function printExactMemoryMatches(config, ov, query, options) {
|
|
7551
|
-
const terms = exactRecallTerms(query);
|
|
7552
|
-
if (terms.length === 0) {
|
|
7553
|
-
return;
|
|
7554
|
-
}
|
|
7555
|
-
const scopes = exactMemoryScopes(config, options.includeArchived);
|
|
7556
|
-
const outputs = [];
|
|
7557
|
-
for (const term of terms) {
|
|
7558
|
-
for (const scope of scopes) {
|
|
7559
|
-
const args = withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5"]);
|
|
7560
|
-
if (options.dryRun) {
|
|
7561
|
-
outputs.push(formatShellCommand(ov, args));
|
|
7562
|
-
continue;
|
|
7563
|
-
}
|
|
7564
|
-
const result = await runCommand(ov, args, { allowFailure: true });
|
|
7565
|
-
const output2 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
7566
|
-
if (result.exitCode === 0 && grepOutputHasMatches(output2)) {
|
|
7567
|
-
outputs.push(output2);
|
|
7568
|
-
}
|
|
7654
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
7655
|
+
const rawContent = await readMemoryContent(config, ov, sourceUri, dryRun);
|
|
7656
|
+
const stripped = stripPersonalProvenance(rawContent);
|
|
7657
|
+
const scrub = applyScrubber(stripped, { redact: options.redact === true });
|
|
7658
|
+
const targetUri = sharedUriFor(config, sourceUri, team.name);
|
|
7659
|
+
if (preview) {
|
|
7660
|
+
console.log(`PREVIEW source: ${sourceUri}`);
|
|
7661
|
+
console.log(`PREVIEW destination: ${targetUri}`);
|
|
7662
|
+
if (scrub.blocker) {
|
|
7663
|
+
console.log(
|
|
7664
|
+
`PREVIEW BLOCKED: ${scrub.blocker}. Strip the sensitive value or rerun with --redact for soft-leak patterns.`
|
|
7665
|
+
);
|
|
7666
|
+
return;
|
|
7569
7667
|
}
|
|
7570
|
-
|
|
7571
|
-
|
|
7668
|
+
for (const redaction of scrub.redactions) {
|
|
7669
|
+
console.log(`PREVIEW redact: ${redaction.count}\xD7 ${redaction.name}`);
|
|
7670
|
+
}
|
|
7671
|
+
console.log("-----BEGIN PREVIEW-----");
|
|
7672
|
+
console.log(scrub.cleaned);
|
|
7673
|
+
console.log("-----END PREVIEW-----");
|
|
7572
7674
|
return;
|
|
7573
7675
|
}
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
}
|
|
7577
|
-
|
|
7578
|
-
if (options.replaceUri) {
|
|
7579
|
-
assertVikingUri(options.replaceUri);
|
|
7676
|
+
if (scrub.blocker) {
|
|
7677
|
+
throw new Error(
|
|
7678
|
+
`Refusing to publish ${sourceUri}: possible ${scrub.blocker}. Strip the sensitive value or pass --redact for soft-leak patterns.`
|
|
7679
|
+
);
|
|
7580
7680
|
}
|
|
7581
|
-
const
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
const
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
const memory = isInPlaceUpdate ? formatMemoryDocument(options.title, finalMetadata, options.bodyText) : candidateMemory;
|
|
7589
|
-
const writeMode = await memoryWriteMode(ov, config, memoryUri, finalMetadata);
|
|
7590
|
-
if (options.dryRun) {
|
|
7591
|
-
console.log(memory);
|
|
7592
|
-
console.log("\nWould run:");
|
|
7593
|
-
console.log(
|
|
7594
|
-
formatShellCommand(
|
|
7595
|
-
ov,
|
|
7596
|
-
withIdentity(config, [
|
|
7597
|
-
"write",
|
|
7598
|
-
memoryUri,
|
|
7599
|
-
"--from-file",
|
|
7600
|
-
memoryPath,
|
|
7601
|
-
"--mode",
|
|
7602
|
-
writeMode,
|
|
7603
|
-
"--wait",
|
|
7604
|
-
"--timeout",
|
|
7605
|
-
"120"
|
|
7606
|
-
])
|
|
7607
|
-
)
|
|
7681
|
+
for (const redaction of scrub.redactions) {
|
|
7682
|
+
console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before publish.`);
|
|
7683
|
+
}
|
|
7684
|
+
const content = scrub.cleaned;
|
|
7685
|
+
if (!dryRun && await vikingResourceExists(ov, config, targetUri)) {
|
|
7686
|
+
throw new Error(
|
|
7687
|
+
`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.`
|
|
7608
7688
|
);
|
|
7609
|
-
if (options.replaceUri && !isInPlaceUpdate) {
|
|
7610
|
-
console.log(formatShellCommand(ov, withIdentity(config, ["rm", options.replaceUri])));
|
|
7611
|
-
}
|
|
7612
|
-
return;
|
|
7613
7689
|
}
|
|
7614
|
-
await (
|
|
7615
|
-
await (
|
|
7616
|
-
await
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7690
|
+
await ensureSharedDirectoryChain(config, ov, targetUri, dryRun);
|
|
7691
|
+
await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
|
|
7692
|
+
await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "publish");
|
|
7693
|
+
const git = await requiredExecutable("git");
|
|
7694
|
+
const worktree = team.config.worktree;
|
|
7695
|
+
const relativePath = vikingUriToWorktreeRelative(config, targetUri, team.name);
|
|
7696
|
+
const message = options.message ?? `share: publish ${relativePath}`;
|
|
7697
|
+
await maybeRun(dryRun, git, ["-C", worktree, "add", "--", relativePath]);
|
|
7698
|
+
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
7699
|
+
if (options.push !== false) {
|
|
7700
|
+
const pushResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
7701
|
+
if (dryRun) {
|
|
7702
|
+
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
7703
|
+
} else if (pushResult && pushResult.exitCode !== 0) {
|
|
7704
|
+
const detail = pushResult.stderr.trim() || pushResult.stdout.trim() || "unknown error";
|
|
7705
|
+
throw new Error(
|
|
7706
|
+
`Memory was committed locally but git push failed: ${detail}
|
|
7707
|
+
Resolve the remote issue (auth, network, branch protection), then run: threadnote share sync`
|
|
7626
7708
|
);
|
|
7627
7709
|
}
|
|
7628
|
-
} else if (isInPlaceUpdate) {
|
|
7629
|
-
console.log(`Updated existing memory in place: ${memoryUri}`);
|
|
7630
7710
|
}
|
|
7711
|
+
console.log(`Published ${sourceUri} -> ${targetUri}`);
|
|
7631
7712
|
}
|
|
7632
|
-
async function
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
"--mode",
|
|
7639
|
-
writeMode,
|
|
7640
|
-
"--wait",
|
|
7641
|
-
"--timeout",
|
|
7642
|
-
"120"
|
|
7643
|
-
]);
|
|
7644
|
-
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
7645
|
-
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
7646
|
-
const result = await runCommand(ov, args, { allowFailure: true });
|
|
7647
|
-
if (result.exitCode === 0) {
|
|
7648
|
-
if (result.stdout.trim()) {
|
|
7649
|
-
console.log(result.stdout.trim());
|
|
7650
|
-
}
|
|
7651
|
-
if (result.stderr.trim()) {
|
|
7652
|
-
console.error(result.stderr.trim());
|
|
7653
|
-
}
|
|
7654
|
-
return;
|
|
7655
|
-
}
|
|
7656
|
-
if (await vikingResourceExists(ov, config, memoryUri)) {
|
|
7657
|
-
console.log("OpenViking accepted the memory but returned before the wait completed; waiting for indexing.");
|
|
7658
|
-
await waitForOpenVikingQueue(ov, config);
|
|
7659
|
-
return;
|
|
7660
|
-
}
|
|
7661
|
-
if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
|
|
7662
|
-
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
7663
|
-
}
|
|
7664
|
-
await sleep(1e3 * (attempt + 1));
|
|
7713
|
+
async function runShareUnpublish(config, sourceUri, options) {
|
|
7714
|
+
assertVikingUri(sourceUri);
|
|
7715
|
+
const team = await resolveTeam(config, options.team);
|
|
7716
|
+
const dryRun = options.dryRun === true;
|
|
7717
|
+
if (!isInTeamNamespace(config, sourceUri, team.name)) {
|
|
7718
|
+
throw new Error(`Memory ${sourceUri} is not in team "${team.name}" shared namespace.`);
|
|
7665
7719
|
}
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
const
|
|
7669
|
-
if (
|
|
7670
|
-
|
|
7720
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
7721
|
+
const content = await readMemoryContent(config, ov, sourceUri, dryRun);
|
|
7722
|
+
const targetUri = personalUriFor(config, sourceUri, team.name);
|
|
7723
|
+
if (!dryRun && await vikingResourceExists(ov, config, targetUri)) {
|
|
7724
|
+
throw new Error(
|
|
7725
|
+
`Refusing to unpublish: a personal memory already exists at ${targetUri}. Move or forget it first, then retry.`
|
|
7726
|
+
);
|
|
7671
7727
|
}
|
|
7672
|
-
|
|
7673
|
-
|
|
7728
|
+
await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
|
|
7729
|
+
await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "unpublish");
|
|
7730
|
+
const git = await requiredExecutable("git");
|
|
7731
|
+
const worktree = team.config.worktree;
|
|
7732
|
+
const relativePath = vikingUriToWorktreeRelative(config, sourceUri, team.name);
|
|
7733
|
+
const message = options.message ?? `share: unpublish ${relativePath}`;
|
|
7734
|
+
await maybeRun(dryRun, git, ["-C", worktree, "rm", relativePath], { allowFailure: true });
|
|
7735
|
+
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
7736
|
+
if (options.push !== false) {
|
|
7737
|
+
await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
7674
7738
|
}
|
|
7739
|
+
console.log(`Unpublished ${sourceUri} -> ${targetUri}`);
|
|
7675
7740
|
}
|
|
7676
|
-
async function
|
|
7677
|
-
const
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
}
|
|
7690
|
-
if (isResourceBusy(result.stderr, result.stdout) && attempt === 3) {
|
|
7691
|
-
return false;
|
|
7692
|
-
}
|
|
7693
|
-
if (!isResourceBusy(result.stderr, result.stdout)) {
|
|
7694
|
-
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
7695
|
-
}
|
|
7696
|
-
await sleep(1e3 * (attempt + 1));
|
|
7741
|
+
async function runShareList(config, _options) {
|
|
7742
|
+
const teams = await readTeamsFile(config);
|
|
7743
|
+
const entries = Object.values(teams.teams);
|
|
7744
|
+
if (entries.length === 0) {
|
|
7745
|
+
console.log("No shared teams configured. Run: threadnote share init <remote-url>");
|
|
7746
|
+
return;
|
|
7747
|
+
}
|
|
7748
|
+
for (const team of entries) {
|
|
7749
|
+
const marker = team.name === teams.defaultTeam ? " (default)" : "";
|
|
7750
|
+
console.log(`- ${team.name}${marker}`);
|
|
7751
|
+
console.log(` remote: ${team.remote}`);
|
|
7752
|
+
console.log(` worktree: ${portablePath(team.worktree)}`);
|
|
7753
|
+
console.log(` gitdir: ${portablePath(team.gitdir)}`);
|
|
7754
|
+
console.log(` added: ${team.addedAt}`);
|
|
7697
7755
|
}
|
|
7698
|
-
return false;
|
|
7699
|
-
}
|
|
7700
|
-
async function vikingResourceExists(ov, config, uri) {
|
|
7701
|
-
const stat3 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
7702
|
-
return stat3.exitCode === 0;
|
|
7703
|
-
}
|
|
7704
|
-
async function ensureDurableMemoryDirectory(ov, config) {
|
|
7705
|
-
await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
|
|
7706
7756
|
}
|
|
7707
|
-
async function
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7757
|
+
async function runShareRemove(config, options) {
|
|
7758
|
+
const team = await resolveTeam(config, options.team);
|
|
7759
|
+
const dryRun = options.dryRun === true;
|
|
7760
|
+
const teamsFile = await readTeamsFile(config);
|
|
7761
|
+
const remaining = {};
|
|
7762
|
+
for (const [name, value] of Object.entries(teamsFile.teams)) {
|
|
7763
|
+
if (name !== team.name) {
|
|
7764
|
+
remaining[name] = value;
|
|
7712
7765
|
}
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7766
|
+
}
|
|
7767
|
+
const remainingNames = Object.keys(remaining);
|
|
7768
|
+
const nextDefault = teamsFile.defaultTeam === team.name ? remainingNames[0] : teamsFile.defaultTeam;
|
|
7769
|
+
const updated = { defaultTeam: nextDefault, teams: remaining, version: TEAMS_FILE_VERSION };
|
|
7770
|
+
if (dryRun) {
|
|
7771
|
+
console.log(`Would update teams file: ${teamsFilePath(config)}`);
|
|
7772
|
+
} else {
|
|
7773
|
+
await writeTeamsFile(config, updated);
|
|
7774
|
+
console.log(`Removed team "${team.name}" from teams.json.`);
|
|
7775
|
+
}
|
|
7776
|
+
if (options.keepFiles !== true) {
|
|
7777
|
+
await removePath(team.config.worktree, "shared worktree", dryRun);
|
|
7778
|
+
await removePath(team.config.gitdir, "shared gitdir", dryRun);
|
|
7779
|
+
} else {
|
|
7780
|
+
console.log(`Keeping files at ${portablePath(team.config.worktree)} and ${portablePath(team.config.gitdir)}`);
|
|
7718
7781
|
}
|
|
7719
7782
|
}
|
|
7720
|
-
function
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
return `${durableMemoryDirectoryUri(config)}/threadnote-migrated-${hash.slice(0, 16)}.md`;
|
|
7725
|
-
}
|
|
7726
|
-
async function hasLegacyLifecycleHandoffCandidates(config) {
|
|
7727
|
-
return (await legacyLifecycleHandoffCandidates(config, 1)).length > 0;
|
|
7728
|
-
}
|
|
7729
|
-
async function legacyLifecycleHandoffCandidates(config, limit) {
|
|
7730
|
-
const eventsRoot = (0, import_node_path4.join)(localUserMemoriesRoot(config), "events");
|
|
7731
|
-
let entries;
|
|
7732
|
-
try {
|
|
7733
|
-
entries = await (0, import_promises4.readdir)(eventsRoot, { withFileTypes: true });
|
|
7734
|
-
} catch (_err) {
|
|
7735
|
-
return [];
|
|
7783
|
+
function normalizeTeamName(input2) {
|
|
7784
|
+
const candidate = (input2 ?? "default").trim();
|
|
7785
|
+
if (!candidate) {
|
|
7786
|
+
return "default";
|
|
7736
7787
|
}
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
}
|
|
7742
|
-
const sourcePath = (0, import_node_path4.join)(eventsRoot, entry.name);
|
|
7743
|
-
const original = await readTextIfExists(sourcePath);
|
|
7744
|
-
if (!original || !isClearLegacyHandoffMemory(original) || sensitiveMemoryReason(original)) {
|
|
7745
|
-
continue;
|
|
7746
|
-
}
|
|
7747
|
-
const sourceUri = `${durableMemoryDirectoryUri(config)}/${entry.name}`;
|
|
7748
|
-
candidates.push({
|
|
7749
|
-
metadata: {
|
|
7750
|
-
archivedFrom: sourceUri,
|
|
7751
|
-
kind: "handoff",
|
|
7752
|
-
project: inferLegacyProject(original),
|
|
7753
|
-
sourceAgentClient: "threadnote",
|
|
7754
|
-
status: "archived",
|
|
7755
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7756
|
-
},
|
|
7757
|
-
original,
|
|
7758
|
-
sourceUri
|
|
7759
|
-
});
|
|
7760
|
-
if (limit !== void 0 && candidates.length >= limit) {
|
|
7761
|
-
break;
|
|
7762
|
-
}
|
|
7788
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(candidate) || /^\.+$/.test(candidate)) {
|
|
7789
|
+
throw new Error(
|
|
7790
|
+
`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.`
|
|
7791
|
+
);
|
|
7763
7792
|
}
|
|
7764
|
-
return
|
|
7793
|
+
return candidate;
|
|
7765
7794
|
}
|
|
7766
|
-
function
|
|
7767
|
-
return
|
|
7795
|
+
function teamsFilePath(config) {
|
|
7796
|
+
return (0, import_node_path4.join)(config.agentContextHome, "share", "teams.json");
|
|
7768
7797
|
}
|
|
7769
|
-
function
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
// grep in a recall surfaces matches in seeded guidance, not only memories.
|
|
7782
|
-
"viking://resources/repos"
|
|
7783
|
-
];
|
|
7784
|
-
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
7798
|
+
function teamWorktreePath(config, team) {
|
|
7799
|
+
return (0, import_node_path4.join)(
|
|
7800
|
+
config.agentContextHome,
|
|
7801
|
+
"data",
|
|
7802
|
+
"viking",
|
|
7803
|
+
config.account,
|
|
7804
|
+
"user",
|
|
7805
|
+
uriSegment(config.user),
|
|
7806
|
+
"memories",
|
|
7807
|
+
SHARED_SEGMENT,
|
|
7808
|
+
team
|
|
7809
|
+
);
|
|
7785
7810
|
}
|
|
7786
|
-
function
|
|
7787
|
-
|
|
7788
|
-
return `${memoryDirectoryUri(config, metadata)}/${filename}`;
|
|
7811
|
+
function teamGitdirPath(config, team) {
|
|
7812
|
+
return (0, import_node_path4.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
|
|
7789
7813
|
}
|
|
7790
|
-
function
|
|
7791
|
-
const
|
|
7792
|
-
const
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
return metadata.status === "active" ? `${baseUri}/preferences` : `${baseUri}/preferences/${uriSegment(metadata.status)}`;
|
|
7796
|
-
case "handoff":
|
|
7797
|
-
return `${baseUri}/handoffs/${uriSegment(metadata.status)}/${projectSegment}`;
|
|
7798
|
-
case "incident":
|
|
7799
|
-
return `${baseUri}/incidents/${uriSegment(metadata.status)}/${projectSegment}`;
|
|
7800
|
-
case "smoke":
|
|
7801
|
-
return `${baseUri}/smoke/${uriSegment(metadata.status)}`;
|
|
7802
|
-
case "durable":
|
|
7803
|
-
return metadata.status === "active" ? `${baseUri}/durable/projects/${projectSegment}` : `${baseUri}/durable/${uriSegment(metadata.status)}/${projectSegment}`;
|
|
7814
|
+
async function readTeamsFile(config) {
|
|
7815
|
+
const path = teamsFilePath(config);
|
|
7816
|
+
const raw = await readFileIfExists(path);
|
|
7817
|
+
if (!raw) {
|
|
7818
|
+
return { teams: {}, version: TEAMS_FILE_VERSION };
|
|
7804
7819
|
}
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
}
|
|
7809
|
-
async function memoryWriteMode(ov, config, memoryUri, metadata) {
|
|
7810
|
-
if (!shouldUseStableMemoryUri(metadata)) {
|
|
7811
|
-
return "create";
|
|
7820
|
+
const parsed = parseJsonConfigObject(raw);
|
|
7821
|
+
if (!parsed) {
|
|
7822
|
+
throw new Error(`Could not parse teams file ${path}`);
|
|
7812
7823
|
}
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
if (!directoryUri.startsWith(prefix)) {
|
|
7818
|
-
return [directoryUri];
|
|
7824
|
+
if (typeof parsed.version === "number" && parsed.version > TEAMS_FILE_VERSION) {
|
|
7825
|
+
throw new Error(
|
|
7826
|
+
`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.`
|
|
7827
|
+
);
|
|
7819
7828
|
}
|
|
7820
|
-
const
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7829
|
+
const teams = {};
|
|
7830
|
+
if (typeof parsed.teams === "object" && parsed.teams !== null && !Array.isArray(parsed.teams)) {
|
|
7831
|
+
for (const [name, value] of Object.entries(parsed.teams)) {
|
|
7832
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
7833
|
+
console.warn(`Skipping non-object team entry "${name}" in ${path}.`);
|
|
7834
|
+
continue;
|
|
7835
|
+
}
|
|
7836
|
+
const entry = value;
|
|
7837
|
+
if (typeof entry.remote !== "string" || entry.remote.length === 0) {
|
|
7838
|
+
console.warn(`Skipping team entry "${name}" in ${path}: missing or empty "remote" field.`);
|
|
7839
|
+
continue;
|
|
7840
|
+
}
|
|
7841
|
+
teams[name] = {
|
|
7842
|
+
addedAt: typeof entry.addedAt === "string" ? entry.addedAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
7843
|
+
gitdir: typeof entry.gitdir === "string" ? entry.gitdir : teamGitdirPath(config, name),
|
|
7844
|
+
name,
|
|
7845
|
+
remote: entry.remote,
|
|
7846
|
+
worktree: typeof entry.worktree === "string" ? entry.worktree : teamWorktreePath(config, name)
|
|
7847
|
+
};
|
|
7848
|
+
}
|
|
7825
7849
|
}
|
|
7826
|
-
|
|
7827
|
-
}
|
|
7828
|
-
function formatMemoryDocument(title, metadata, body) {
|
|
7829
|
-
const header = [
|
|
7830
|
-
title,
|
|
7831
|
-
`kind: ${metadata.kind}`,
|
|
7832
|
-
`status: ${metadata.status}`,
|
|
7833
|
-
metadata.project ? `project: ${metadata.project}` : void 0,
|
|
7834
|
-
metadata.topic ? `topic: ${metadata.topic}` : void 0,
|
|
7835
|
-
`source_agent_client: ${metadata.sourceAgentClient}`,
|
|
7836
|
-
`timestamp: ${metadata.timestamp}`,
|
|
7837
|
-
metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
|
|
7838
|
-
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
|
|
7839
|
-
].filter((line) => line !== void 0);
|
|
7840
|
-
return [...header, "", body.trim()].join("\n");
|
|
7850
|
+
const defaultTeam = typeof parsed.defaultTeam === "string" ? parsed.defaultTeam : void 0;
|
|
7851
|
+
return { defaultTeam, teams, version: TEAMS_FILE_VERSION };
|
|
7841
7852
|
}
|
|
7842
|
-
function
|
|
7843
|
-
const
|
|
7844
|
-
|
|
7845
|
-
const
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
return {
|
|
7850
|
-
kind,
|
|
7851
|
-
project,
|
|
7852
|
-
sourceAgentClient: parseHeaderValue(header, "source_agent_client") ?? void 0,
|
|
7853
|
-
status,
|
|
7854
|
-
topic
|
|
7853
|
+
async function writeTeamsFile(config, contents) {
|
|
7854
|
+
const path = teamsFilePath(config);
|
|
7855
|
+
await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
7856
|
+
const serializable = {
|
|
7857
|
+
defaultTeam: contents.defaultTeam,
|
|
7858
|
+
teams: contents.teams,
|
|
7859
|
+
version: contents.version
|
|
7855
7860
|
};
|
|
7861
|
+
await (0, import_promises4.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
|
|
7862
|
+
`, { encoding: "utf8", mode: 384 });
|
|
7856
7863
|
}
|
|
7857
|
-
function
|
|
7858
|
-
const
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
if (/^kind:\s*/m.test(memory) || /^status:\s*/m.test(memory)) {
|
|
7863
|
-
return false;
|
|
7864
|
-
}
|
|
7865
|
-
const trimmed = memory.trim();
|
|
7866
|
-
if (trimmed.startsWith("HANDOFF\n")) {
|
|
7867
|
-
return true;
|
|
7864
|
+
async function resolveTeam(config, requested) {
|
|
7865
|
+
const teamsFile = await readTeamsFile(config);
|
|
7866
|
+
const entries = Object.entries(teamsFile.teams);
|
|
7867
|
+
if (entries.length === 0) {
|
|
7868
|
+
throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
|
|
7868
7869
|
}
|
|
7869
|
-
|
|
7870
|
-
|
|
7870
|
+
const wantName = requested ? normalizeTeamName(requested) : teamsFile.defaultTeam ?? entries[0][0];
|
|
7871
|
+
const found = teamsFile.teams[wantName];
|
|
7872
|
+
if (!found) {
|
|
7873
|
+
const known = entries.map(([name]) => name).join(", ");
|
|
7874
|
+
throw new Error(`Team "${wantName}" is not configured. Known teams: ${known}`);
|
|
7871
7875
|
}
|
|
7872
|
-
return
|
|
7873
|
-
}
|
|
7874
|
-
function memoryBody(memory) {
|
|
7875
|
-
const separatorIndex = memory.indexOf("\n\n");
|
|
7876
|
-
return separatorIndex === -1 ? "" : memory.slice(separatorIndex + 2).trim();
|
|
7876
|
+
return { config: found, name: wantName };
|
|
7877
7877
|
}
|
|
7878
|
-
function
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
return "general";
|
|
7878
|
+
function shouldSetDefault(options, existing) {
|
|
7879
|
+
if (options.setDefault === true) {
|
|
7880
|
+
return true;
|
|
7882
7881
|
}
|
|
7883
|
-
|
|
7884
|
-
return trimmed.includes("/") ? (0, import_node_path4.basename)(trimmed) : trimmed;
|
|
7882
|
+
return existing.defaultTeam === void 0;
|
|
7885
7883
|
}
|
|
7886
|
-
function
|
|
7887
|
-
if (!
|
|
7888
|
-
return
|
|
7884
|
+
async function assertWorktreeUsable(worktree) {
|
|
7885
|
+
if (!await exists(worktree)) {
|
|
7886
|
+
return;
|
|
7889
7887
|
}
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
} catch (_err) {
|
|
7893
|
-
return void 0;
|
|
7888
|
+
if (!await isDirectory(worktree)) {
|
|
7889
|
+
throw new Error(`Cannot use ${worktree} as a worktree: not a directory.`);
|
|
7894
7890
|
}
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7891
|
+
const entries = await (0, import_promises4.readdir)(worktree);
|
|
7892
|
+
if (entries.length > 0) {
|
|
7893
|
+
const preview = entries.slice(0, 5).join(", ");
|
|
7894
|
+
const suffix = entries.length > 5 ? `, +${entries.length - 5} more` : "";
|
|
7895
|
+
throw new Error(
|
|
7896
|
+
`Worktree ${worktree} is not empty (contains: ${preview}${suffix}). Move or remove its contents, then retry threadnote share init.`
|
|
7897
|
+
);
|
|
7899
7898
|
}
|
|
7900
|
-
try {
|
|
7901
|
-
return parseMemoryStatus(value);
|
|
7902
|
-
} catch (_err) {
|
|
7903
|
-
return void 0;
|
|
7904
|
-
}
|
|
7905
|
-
}
|
|
7906
|
-
function normalizeOptionalMetadata(value) {
|
|
7907
|
-
const trimmed = value?.trim();
|
|
7908
|
-
return trimmed ? trimmed : void 0;
|
|
7909
7899
|
}
|
|
7910
|
-
async function
|
|
7911
|
-
const
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
return accounts.filter((account) => !account.startsWith("_"));
|
|
7900
|
+
async function ingestWorktreeFiles(config, team, initialMode) {
|
|
7901
|
+
const ov = await openVikingCliForMode(false);
|
|
7902
|
+
const files = await walkMemoryFiles(team.worktree);
|
|
7903
|
+
for (const file of files) {
|
|
7904
|
+
const uri = workfileToVikingUri(config, team, file);
|
|
7905
|
+
await ensureSharedDirectoryChain(config, ov, uri, false);
|
|
7906
|
+
await ingestSingleFile(ov, config, uri, file, initialMode);
|
|
7918
7907
|
}
|
|
7919
|
-
return
|
|
7908
|
+
return files.length;
|
|
7920
7909
|
}
|
|
7921
|
-
async function
|
|
7922
|
-
const
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7910
|
+
async function walkMemoryFiles(root) {
|
|
7911
|
+
const out = [];
|
|
7912
|
+
async function visit(path, depth) {
|
|
7913
|
+
let entries;
|
|
7914
|
+
try {
|
|
7915
|
+
entries = await (0, import_promises4.readdir)(path, { withFileTypes: true });
|
|
7916
|
+
} catch (err) {
|
|
7917
|
+
console.warn(`Skipping ${path} during shared-tree walk: ${err instanceof Error ? err.message : String(err)}`);
|
|
7918
|
+
return;
|
|
7919
|
+
}
|
|
7920
|
+
for (const entry of entries) {
|
|
7921
|
+
if (entry.name === ".git") {
|
|
7922
|
+
continue;
|
|
7923
|
+
}
|
|
7924
|
+
const full = (0, import_node_path4.join)(path, entry.name);
|
|
7925
|
+
if (entry.isDirectory()) {
|
|
7926
|
+
if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
|
|
7929
7927
|
continue;
|
|
7930
7928
|
}
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7929
|
+
await visit(full, depth + 1);
|
|
7930
|
+
continue;
|
|
7931
|
+
}
|
|
7932
|
+
if (!entry.isFile()) {
|
|
7933
|
+
continue;
|
|
7934
|
+
}
|
|
7935
|
+
if (depth === 0) {
|
|
7936
|
+
continue;
|
|
7937
|
+
}
|
|
7938
|
+
if (!entry.name.endsWith(".md")) {
|
|
7939
|
+
continue;
|
|
7942
7940
|
}
|
|
7941
|
+
out.push(full);
|
|
7943
7942
|
}
|
|
7944
7943
|
}
|
|
7945
|
-
|
|
7944
|
+
await visit(root, 0);
|
|
7945
|
+
return out;
|
|
7946
7946
|
}
|
|
7947
|
-
|
|
7948
|
-
const
|
|
7949
|
-
|
|
7950
|
-
|
|
7947
|
+
function workfileToVikingUri(config, team, filePath) {
|
|
7948
|
+
const rel = (0, import_node_path4.relative)(team.worktree, filePath).split(import_node_path4.sep).join("/");
|
|
7949
|
+
return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
7950
|
+
}
|
|
7951
|
+
function isInSharedNamespace(config, uri) {
|
|
7952
|
+
return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`);
|
|
7953
|
+
}
|
|
7954
|
+
function isInTeamNamespace(config, uri, team) {
|
|
7955
|
+
return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`);
|
|
7956
|
+
}
|
|
7957
|
+
function sharedUriFor(config, personalUri, team) {
|
|
7958
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/`;
|
|
7959
|
+
if (!personalUri.startsWith(prefix)) {
|
|
7960
|
+
throw new Error(`Refusing to publish memory outside the current user namespace: ${personalUri}`);
|
|
7951
7961
|
}
|
|
7952
|
-
const
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7962
|
+
const rest = personalUri.slice(prefix.length);
|
|
7963
|
+
return `${prefix}${SHARED_SEGMENT}/${team}/${rest}`;
|
|
7964
|
+
}
|
|
7965
|
+
function personalUriFor(config, sharedUri, team) {
|
|
7966
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`;
|
|
7967
|
+
if (!sharedUri.startsWith(prefix)) {
|
|
7968
|
+
throw new Error(`Refusing to unpublish a URI outside team "${team}" shared namespace: ${sharedUri}`);
|
|
7969
|
+
}
|
|
7970
|
+
const rest = sharedUri.slice(prefix.length);
|
|
7971
|
+
return `viking://user/${uriSegment(config.user)}/memories/${rest}`;
|
|
7972
|
+
}
|
|
7973
|
+
function vikingUriToWorktreeRelative(config, uri, team) {
|
|
7974
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`;
|
|
7975
|
+
if (!uri.startsWith(prefix)) {
|
|
7976
|
+
throw new Error(`URI ${uri} is not inside team "${team}" shared subtree.`);
|
|
7977
|
+
}
|
|
7978
|
+
return uri.slice(prefix.length);
|
|
7979
|
+
}
|
|
7980
|
+
function stripPersonalProvenance(content) {
|
|
7981
|
+
const lines = content.split("\n");
|
|
7982
|
+
let headerEnd = lines.length;
|
|
7983
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
7984
|
+
if (lines[index].trim() === "") {
|
|
7985
|
+
headerEnd = index;
|
|
7986
|
+
break;
|
|
7957
7987
|
}
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
memories.push(text);
|
|
7963
|
-
}
|
|
7964
|
-
} catch (_err) {
|
|
7988
|
+
}
|
|
7989
|
+
const cleaned = [];
|
|
7990
|
+
for (let index = 0; index < headerEnd; index += 1) {
|
|
7991
|
+
if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
|
|
7965
7992
|
continue;
|
|
7966
7993
|
}
|
|
7994
|
+
cleaned.push(lines[index]);
|
|
7967
7995
|
}
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
function legacyMessageText(value) {
|
|
7971
|
-
if (!isJsonObject(value)) {
|
|
7972
|
-
return void 0;
|
|
7996
|
+
for (let index = headerEnd; index < lines.length; index += 1) {
|
|
7997
|
+
cleaned.push(lines[index]);
|
|
7973
7998
|
}
|
|
7974
|
-
|
|
7975
|
-
|
|
7999
|
+
return cleaned.join("\n");
|
|
8000
|
+
}
|
|
8001
|
+
async function readMemoryContent(config, ov, uri, dryRun) {
|
|
8002
|
+
const args = withIdentity(config, ["read", uri]);
|
|
8003
|
+
if (dryRun) {
|
|
8004
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8005
|
+
return "<dry-run memory body>";
|
|
7976
8006
|
}
|
|
7977
|
-
|
|
7978
|
-
|
|
8007
|
+
const result = await runCommand(ov, args);
|
|
8008
|
+
if (!result.stdout.trim()) {
|
|
8009
|
+
throw new Error(`Refusing to publish empty memory at ${uri}`);
|
|
7979
8010
|
}
|
|
7980
|
-
|
|
7981
|
-
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
7982
|
-
}
|
|
7983
|
-
function isLegacyThreadnoteMemory(text) {
|
|
7984
|
-
return text.startsWith("MEMORY\n") || text.startsWith("HANDOFF\n");
|
|
7985
|
-
}
|
|
7986
|
-
async function existingDurableMemoryHashes(config) {
|
|
7987
|
-
const hashes = /* @__PURE__ */ new Set();
|
|
7988
|
-
await collectDurableMemoryHashes(localVikingDataRoot(config), hashes);
|
|
7989
|
-
return hashes;
|
|
8011
|
+
return result.stdout;
|
|
7990
8012
|
}
|
|
7991
|
-
async function
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
const path = (0, import_node_path4.join)(root, entry.name);
|
|
8000
|
-
if (entry.isDirectory()) {
|
|
8001
|
-
await collectDurableMemoryHashes(path, hashes);
|
|
8013
|
+
async function ensureSharedDirectoryChain(config, ov, memoryUri, dryRun, options = {}) {
|
|
8014
|
+
const directoryUri = parentUri(memoryUri);
|
|
8015
|
+
for (const uri of sharedDirectoryChain(config, directoryUri)) {
|
|
8016
|
+
const args = withIdentity(config, ["stat", uri]);
|
|
8017
|
+
if (dryRun) {
|
|
8018
|
+
if (options.quiet !== true) {
|
|
8019
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8020
|
+
}
|
|
8002
8021
|
continue;
|
|
8003
8022
|
}
|
|
8004
|
-
|
|
8023
|
+
const statResult = await runCommand(ov, args, { allowFailure: true });
|
|
8024
|
+
if (statResult.exitCode === 0) {
|
|
8005
8025
|
continue;
|
|
8006
8026
|
}
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
hashes.add(sha256(comparableMemoryText(trimmedContent)));
|
|
8027
|
+
if (options.quiet === true) {
|
|
8028
|
+
await runCommand(ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared memories."]));
|
|
8029
|
+
} else {
|
|
8030
|
+
await maybeRun(false, ov, withIdentity(config, ["mkdir", uri, "--description", "Threadnote shared memories."]));
|
|
8012
8031
|
}
|
|
8013
8032
|
}
|
|
8014
8033
|
}
|
|
8015
|
-
function
|
|
8016
|
-
|
|
8034
|
+
function parentUri(uri) {
|
|
8035
|
+
const lastSlash = uri.lastIndexOf("/");
|
|
8036
|
+
return lastSlash === -1 ? uri : uri.slice(0, lastSlash);
|
|
8017
8037
|
}
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
} catch (_err) {
|
|
8023
|
-
return [];
|
|
8038
|
+
function sharedDirectoryChain(config, directoryUri) {
|
|
8039
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
|
|
8040
|
+
if (!directoryUri.startsWith(prefix)) {
|
|
8041
|
+
return [directoryUri];
|
|
8024
8042
|
}
|
|
8025
|
-
|
|
8043
|
+
const parts = directoryUri.slice(prefix.length).split("/").filter(Boolean);
|
|
8044
|
+
const chain = [];
|
|
8045
|
+
for (let index = 1; index <= parts.length; index += 1) {
|
|
8046
|
+
chain.push(`${prefix}${parts.slice(0, index).join("/")}`);
|
|
8047
|
+
}
|
|
8048
|
+
return chain;
|
|
8026
8049
|
}
|
|
8027
|
-
async function
|
|
8028
|
-
|
|
8029
|
-
const
|
|
8030
|
-
|
|
8031
|
-
|
|
8050
|
+
async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun, options = {}) {
|
|
8051
|
+
if (dryRun) {
|
|
8052
|
+
const args = withIdentity(config, [
|
|
8053
|
+
"write",
|
|
8054
|
+
uri,
|
|
8055
|
+
"--from-file",
|
|
8056
|
+
"<staged temp file>",
|
|
8057
|
+
"--mode",
|
|
8058
|
+
initialMode,
|
|
8059
|
+
"--wait",
|
|
8060
|
+
"--timeout",
|
|
8061
|
+
"120"
|
|
8062
|
+
]);
|
|
8063
|
+
if (options.quiet !== true) {
|
|
8064
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8032
8065
|
}
|
|
8033
|
-
return
|
|
8034
|
-
} catch (_err) {
|
|
8035
|
-
return void 0;
|
|
8066
|
+
return;
|
|
8036
8067
|
}
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
{
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
{
|
|
8044
|
-
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ }
|
|
8045
|
-
];
|
|
8046
|
-
return patterns.find((pattern) => pattern.regex.test(text))?.name;
|
|
8047
|
-
}
|
|
8048
|
-
function comparableMemoryText(text) {
|
|
8049
|
-
const trimmed = text.trim();
|
|
8050
|
-
if (!trimmed.startsWith("MEMORY\n")) {
|
|
8051
|
-
return trimmed;
|
|
8068
|
+
const stagingDir = await (0, import_promises4.mkdtemp)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
|
|
8069
|
+
const tempPath = (0, import_node_path4.join)(stagingDir, "body.txt");
|
|
8070
|
+
try {
|
|
8071
|
+
await (0, import_promises4.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
|
|
8072
|
+
await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode, options);
|
|
8073
|
+
} finally {
|
|
8074
|
+
await (0, import_promises4.rm)(stagingDir, { force: true, recursive: true });
|
|
8052
8075
|
}
|
|
8053
|
-
const separatorIndex = trimmed.indexOf("\n\n");
|
|
8054
|
-
return separatorIndex === -1 ? trimmed : trimmed.slice(separatorIndex + 2).trim();
|
|
8055
|
-
}
|
|
8056
|
-
function legacySourceLabel(candidate) {
|
|
8057
|
-
return `${candidate.sourceAccount}/${candidate.sourceSession}/${candidate.sourceArchive}`;
|
|
8058
|
-
}
|
|
8059
|
-
function localVikingDataRoot(config) {
|
|
8060
|
-
return (0, import_node_path4.join)(config.agentContextHome, "data", "viking");
|
|
8061
|
-
}
|
|
8062
|
-
function localUserMemoriesRoot(config) {
|
|
8063
|
-
return (0, import_node_path4.join)(localVikingDataRoot(config), config.account, "user", uriSegment(config.user), "memories");
|
|
8064
|
-
}
|
|
8065
|
-
function uniqueStrings(values) {
|
|
8066
|
-
return [...new Set(values)].sort();
|
|
8067
|
-
}
|
|
8068
|
-
function isResourceBusy(stderr, stdout) {
|
|
8069
|
-
const output2 = `${stderr}
|
|
8070
|
-
${stdout}`.toLowerCase();
|
|
8071
|
-
return output2.includes("resource is busy") || output2.includes("resource is being processed");
|
|
8072
8076
|
}
|
|
8073
|
-
async function
|
|
8074
|
-
const
|
|
8075
|
-
const
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
"next_step:",
|
|
8110
|
-
options.nextStep ?? "- inspect the current repo state and continue from this handoff"
|
|
8111
|
-
].join("\n");
|
|
8112
|
-
return { bodyText, metadata };
|
|
8113
|
-
}
|
|
8114
|
-
async function gitTouchedFiles(cwd) {
|
|
8115
|
-
const changedFiles = await gitValue(["diff", "--name-only", "HEAD"], cwd);
|
|
8116
|
-
const untrackedFiles = await gitValue(["ls-files", "--others", "--exclude-standard"], cwd);
|
|
8117
|
-
const files = /* @__PURE__ */ new Set();
|
|
8118
|
-
for (const value of [changedFiles, untrackedFiles]) {
|
|
8119
|
-
for (const line of (value ?? "").split("\n")) {
|
|
8120
|
-
const trimmed = line.trim();
|
|
8121
|
-
if (trimmed) {
|
|
8122
|
-
files.add(trimmed);
|
|
8077
|
+
async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode, options = {}) {
|
|
8078
|
+
const maxAttempts = 4;
|
|
8079
|
+
const existedBeforeWrite = await vikingResourceExists(ov, config, uri);
|
|
8080
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
8081
|
+
const existsNow = attempt === 0 ? existedBeforeWrite : await vikingResourceExists(ov, config, uri);
|
|
8082
|
+
const ourWriteLanded = existsNow && !existedBeforeWrite;
|
|
8083
|
+
const mode = attempt === 0 ? initialMode : ourWriteLanded ? "replace" : initialMode;
|
|
8084
|
+
const args = withIdentity(config, [
|
|
8085
|
+
"write",
|
|
8086
|
+
uri,
|
|
8087
|
+
"--from-file",
|
|
8088
|
+
fromFile,
|
|
8089
|
+
"--mode",
|
|
8090
|
+
mode,
|
|
8091
|
+
"--wait",
|
|
8092
|
+
"--timeout",
|
|
8093
|
+
"120"
|
|
8094
|
+
]);
|
|
8095
|
+
if (options.quiet !== true) {
|
|
8096
|
+
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
8097
|
+
}
|
|
8098
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
8099
|
+
if (result.exitCode === 0) {
|
|
8100
|
+
if (options.quiet !== true && result.stdout.trim()) {
|
|
8101
|
+
console.log(result.stdout.trim());
|
|
8102
|
+
}
|
|
8103
|
+
if (options.quiet !== true && result.stderr.trim()) {
|
|
8104
|
+
console.error(result.stderr.trim());
|
|
8105
|
+
}
|
|
8106
|
+
return;
|
|
8107
|
+
}
|
|
8108
|
+
if (isTransientOvFailure(result.stderr, result.stdout) && await vikingResourceExists(ov, config, uri) && !existedBeforeWrite) {
|
|
8109
|
+
if (options.quiet !== true) {
|
|
8110
|
+
console.log(
|
|
8111
|
+
"OpenViking accepted the write but returned an error before the wait completed; draining the queue."
|
|
8112
|
+
);
|
|
8123
8113
|
}
|
|
8114
|
+
await waitForOvQueue(ov, config, options);
|
|
8115
|
+
return;
|
|
8124
8116
|
}
|
|
8117
|
+
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
8118
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
8119
|
+
}
|
|
8120
|
+
await sleep(1e3 * (attempt + 1));
|
|
8125
8121
|
}
|
|
8126
|
-
return [...files].sort().join("\n");
|
|
8127
8122
|
}
|
|
8128
|
-
function
|
|
8129
|
-
const
|
|
8130
|
-
if (
|
|
8131
|
-
|
|
8123
|
+
async function waitForOvQueue(ov, config, options = {}) {
|
|
8124
|
+
const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
|
|
8125
|
+
if (options.quiet !== true && result.stdout.trim()) {
|
|
8126
|
+
console.log(result.stdout.trim());
|
|
8127
|
+
}
|
|
8128
|
+
if (options.quiet !== true && result.stderr.trim()) {
|
|
8129
|
+
console.error(result.stderr.trim());
|
|
8132
8130
|
}
|
|
8133
|
-
return trimmed.split("\n").map((line) => `- ${line}`).join("\n");
|
|
8134
8131
|
}
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8132
|
+
function isTransientOvFailure(stderr, stdout) {
|
|
8133
|
+
const output2 = `${stderr}
|
|
8134
|
+
${stdout}`.toLowerCase();
|
|
8135
|
+
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");
|
|
8136
|
+
}
|
|
8137
|
+
async function ingestSingleFile(ov, config, uri, filePath, initialMode, options = {}) {
|
|
8138
|
+
const content = await (0, import_promises4.readFile)(filePath, "utf8");
|
|
8139
|
+
await writeMemoryFile(config, ov, uri, content, initialMode, false, options);
|
|
8140
|
+
}
|
|
8141
|
+
async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
|
|
8142
|
+
try {
|
|
8143
|
+
await removeMemoryUri(config, ov, sourceUri, dryRun);
|
|
8144
|
+
} catch (sourceErr) {
|
|
8145
|
+
if (dryRun) {
|
|
8146
|
+
throw sourceErr;
|
|
8147
|
+
}
|
|
8148
|
+
console.error(
|
|
8149
|
+
`Source removal failed during ${label}; rolling back ${rollbackUri} so the system is back to the pre-${label} state.`
|
|
8150
|
+
);
|
|
8151
|
+
try {
|
|
8152
|
+
await removeMemoryUri(config, ov, rollbackUri, false);
|
|
8153
|
+
} catch (rollbackErr) {
|
|
8154
|
+
console.error(
|
|
8155
|
+
`Rollback of ${rollbackUri} also failed. Manual cleanup needed via: threadnote forget ${rollbackUri}
|
|
8156
|
+
Rollback error: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`
|
|
8157
|
+
);
|
|
8158
|
+
}
|
|
8159
|
+
await bestEffortRemoveWorktreeFile(rollbackUri, worktree, label);
|
|
8160
|
+
throw sourceErr;
|
|
8146
8161
|
}
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8162
|
+
}
|
|
8163
|
+
async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
8164
|
+
if (label !== "publish") {
|
|
8165
|
+
return;
|
|
8150
8166
|
}
|
|
8151
|
-
const
|
|
8152
|
-
if (
|
|
8153
|
-
|
|
8154
|
-
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8155
|
-
latestVersion: fresh,
|
|
8156
|
-
version: 1
|
|
8157
|
-
});
|
|
8158
|
-
return compareVersions(args.currentVersion, fresh);
|
|
8167
|
+
const prefix = "viking://";
|
|
8168
|
+
if (!rollbackUri.startsWith(prefix)) {
|
|
8169
|
+
return;
|
|
8159
8170
|
}
|
|
8160
|
-
|
|
8161
|
-
|
|
8171
|
+
const parts = rollbackUri.slice(prefix.length).split("/");
|
|
8172
|
+
const sharedIndex = parts.indexOf("shared");
|
|
8173
|
+
if (sharedIndex === -1 || sharedIndex + 2 >= parts.length) {
|
|
8174
|
+
return;
|
|
8162
8175
|
}
|
|
8163
|
-
|
|
8176
|
+
const relative3 = parts.slice(sharedIndex + 2).join("/");
|
|
8177
|
+
if (!relative3) {
|
|
8178
|
+
return;
|
|
8179
|
+
}
|
|
8180
|
+
await (0, import_promises4.rm)((0, import_node_path4.join)(worktree, relative3), { force: true });
|
|
8164
8181
|
}
|
|
8165
|
-
function
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
if (
|
|
8169
|
-
|
|
8182
|
+
async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
|
|
8183
|
+
const args = withIdentity(config, ["rm", uri]);
|
|
8184
|
+
if (dryRun) {
|
|
8185
|
+
if (options.quiet !== true) {
|
|
8186
|
+
console.log(`Would run: ${formatShellCommand(ov, args)}`);
|
|
8170
8187
|
}
|
|
8171
|
-
|
|
8172
|
-
detached: true,
|
|
8173
|
-
stdio: "ignore"
|
|
8174
|
-
});
|
|
8175
|
-
child.unref();
|
|
8176
|
-
} catch {
|
|
8188
|
+
return;
|
|
8177
8189
|
}
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
function isNewerVersion(candidate, baseline) {
|
|
8187
|
-
const candidateParts = parseVersion(candidate);
|
|
8188
|
-
const baselineParts = parseVersion(baseline);
|
|
8189
|
-
for (let index = 0; index < 3; index += 1) {
|
|
8190
|
-
if (candidateParts[index] !== baselineParts[index]) {
|
|
8191
|
-
return candidateParts[index] > baselineParts[index];
|
|
8190
|
+
const maxAttempts = 4;
|
|
8191
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
8192
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
8193
|
+
if (result.exitCode === 0) {
|
|
8194
|
+
if (options.quiet !== true && result.stdout.trim()) {
|
|
8195
|
+
console.log(result.stdout.trim());
|
|
8196
|
+
}
|
|
8197
|
+
return;
|
|
8192
8198
|
}
|
|
8199
|
+
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
8200
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
8201
|
+
}
|
|
8202
|
+
await sleep(1e3 * (attempt + 1));
|
|
8193
8203
|
}
|
|
8194
|
-
return false;
|
|
8195
8204
|
}
|
|
8196
|
-
function
|
|
8197
|
-
const
|
|
8198
|
-
return
|
|
8205
|
+
async function vikingResourceExists(ov, config, uri) {
|
|
8206
|
+
const result = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
8207
|
+
return result.exitCode === 0;
|
|
8199
8208
|
}
|
|
8200
|
-
function
|
|
8201
|
-
const
|
|
8202
|
-
return
|
|
8209
|
+
async function hasUncommittedChanges(worktree) {
|
|
8210
|
+
const result = await runCommand("git", ["-C", worktree, "status", "--porcelain"], { allowFailure: true });
|
|
8211
|
+
return result.stdout.trim().length > 0;
|
|
8203
8212
|
}
|
|
8204
|
-
async function
|
|
8205
|
-
|
|
8206
|
-
const raw = await (0, import_promises5.readFile)(cachePath, "utf8");
|
|
8207
|
-
const parsed = JSON.parse(raw);
|
|
8208
|
-
if (parsed.version !== 1 || typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
|
|
8209
|
-
return void 0;
|
|
8210
|
-
}
|
|
8211
|
-
return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, version: 1 };
|
|
8212
|
-
} catch {
|
|
8213
|
+
async function gitOutput(worktree, args, dryRun) {
|
|
8214
|
+
if (dryRun) {
|
|
8213
8215
|
return void 0;
|
|
8214
8216
|
}
|
|
8215
|
-
}
|
|
8216
|
-
|
|
8217
|
-
try {
|
|
8218
|
-
await (0, import_promises5.mkdir)((0, import_node_path5.dirname)(cachePath), { recursive: true });
|
|
8219
|
-
await (0, import_promises5.writeFile)(cachePath, `${JSON.stringify(contents)}
|
|
8220
|
-
`, { encoding: "utf8", mode: 384 });
|
|
8221
|
-
} catch {
|
|
8222
|
-
}
|
|
8223
|
-
}
|
|
8224
|
-
async function fetchLatestVersion() {
|
|
8225
|
-
try {
|
|
8226
|
-
const response = await fetch(NPM_LATEST_URL, {
|
|
8227
|
-
headers: { accept: "application/json" },
|
|
8228
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
8229
|
-
});
|
|
8230
|
-
if (!response.ok) {
|
|
8231
|
-
return void 0;
|
|
8232
|
-
}
|
|
8233
|
-
const data = await response.json();
|
|
8234
|
-
return typeof data.version === "string" && data.version.length > 0 ? data.version : void 0;
|
|
8235
|
-
} catch {
|
|
8217
|
+
const result = await runCommand("git", ["-C", worktree, ...args], { allowFailure: true });
|
|
8218
|
+
if (result.exitCode !== 0) {
|
|
8236
8219
|
return void 0;
|
|
8237
8220
|
}
|
|
8221
|
+
return result.stdout.trim();
|
|
8238
8222
|
}
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
if (cachedVersion !== void 0) {
|
|
8246
|
-
return cachedVersion;
|
|
8223
|
+
async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
8224
|
+
const result = await runCommand("git", ["-C", worktree, "diff", "--name-status", "-z", `${beforeRev}..${afterRev}`], {
|
|
8225
|
+
allowFailure: true
|
|
8226
|
+
});
|
|
8227
|
+
if (result.exitCode !== 0) {
|
|
8228
|
+
return [];
|
|
8247
8229
|
}
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
}
|
|
8270
|
-
];
|
|
8271
|
-
async function runHooksInstall(config, agent, options) {
|
|
8272
|
-
const apply = options.apply === true && options.dryRun !== true;
|
|
8273
|
-
const remove = options.remove === true;
|
|
8274
|
-
switch (agent) {
|
|
8275
|
-
case "claude":
|
|
8276
|
-
await runClaudeHooksInstall({ apply, remove });
|
|
8277
|
-
return;
|
|
8278
|
-
case "codex":
|
|
8279
|
-
printCodexHooksNotice(remove);
|
|
8280
|
-
return;
|
|
8281
|
-
case "cursor":
|
|
8282
|
-
printNoHooksSupported("cursor", remove);
|
|
8283
|
-
return;
|
|
8284
|
-
case "copilot":
|
|
8285
|
-
printNoHooksSupported("copilot", remove);
|
|
8286
|
-
return;
|
|
8287
|
-
}
|
|
8288
|
-
}
|
|
8289
|
-
async function runClaudeHooksInstall(options) {
|
|
8290
|
-
const path = expandPath(CLAUDE_SETTINGS_PATH);
|
|
8291
|
-
const existingRaw = await exists(path) ? await (0, import_promises6.readFile)(path, "utf8") : "{}";
|
|
8292
|
-
const parsed = parseJsonConfigObject(existingRaw) ?? {};
|
|
8293
|
-
const next = options.remove ? withoutThreadnoteHooks(parsed) : withThreadnoteHooks(parsed);
|
|
8294
|
-
const before = JSON.stringify(parsed);
|
|
8295
|
-
const after = JSON.stringify(next);
|
|
8296
|
-
if (before === after) {
|
|
8297
|
-
console.log(`Claude hooks already ${options.remove ? "absent" : "managed"} in ${path}.`);
|
|
8298
|
-
return;
|
|
8299
|
-
}
|
|
8300
|
-
console.log(`${options.apply ? "Updating" : "Would update"} ${path}:`);
|
|
8301
|
-
for (const entry of MANAGED_HOOKS) {
|
|
8302
|
-
console.log(` ${options.remove ? "-" : "+"} ${entry.event}: ${entry.command}`);
|
|
8303
|
-
}
|
|
8304
|
-
if (!options.apply) {
|
|
8305
|
-
console.log("\nRe-run with --apply to actually modify the file.");
|
|
8306
|
-
return;
|
|
8307
|
-
}
|
|
8308
|
-
await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(path), { recursive: true });
|
|
8309
|
-
const serialized = `${JSON.stringify(next, void 0, 2)}
|
|
8310
|
-
`;
|
|
8311
|
-
await (0, import_promises6.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
|
|
8312
|
-
await (0, import_promises6.chmod)(path, 384);
|
|
8313
|
-
console.log(`${options.remove ? "Removed" : "Installed"} threadnote-managed Claude hooks.`);
|
|
8314
|
-
}
|
|
8315
|
-
function withThreadnoteHooks(input2) {
|
|
8316
|
-
const hooks = ensureMutableObject(input2.hooks);
|
|
8317
|
-
for (const entry of MANAGED_HOOKS) {
|
|
8318
|
-
const list = ensureMutableArray(hooks[entry.event]).filter((item) => !isManagedThreadnoteEntry(item));
|
|
8319
|
-
list.push({
|
|
8320
|
-
[THREADNOTE_HOOK_MARKER]: THREADNOTE_HOOK_MARKER_VALUE,
|
|
8321
|
-
matcher: "",
|
|
8322
|
-
hooks: [{ type: "command", command: entry.command }]
|
|
8323
|
-
});
|
|
8324
|
-
hooks[entry.event] = list;
|
|
8230
|
+
const entries = result.stdout.split("\0").filter((part) => part.length > 0);
|
|
8231
|
+
const changes = [];
|
|
8232
|
+
for (let index = 0; index < entries.length; ) {
|
|
8233
|
+
const raw = entries[index];
|
|
8234
|
+
const head = raw.slice(0, 1);
|
|
8235
|
+
if (head === "R" || head === "C") {
|
|
8236
|
+
const oldRel = entries[index + 1];
|
|
8237
|
+
const newRel = entries[index + 2];
|
|
8238
|
+
if (oldRel && newRel) {
|
|
8239
|
+
changes.push({ path: (0, import_node_path4.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
8240
|
+
changes.push({ path: (0, import_node_path4.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
8241
|
+
}
|
|
8242
|
+
index += 3;
|
|
8243
|
+
continue;
|
|
8244
|
+
}
|
|
8245
|
+
const rel = entries[index + 1];
|
|
8246
|
+
if (rel) {
|
|
8247
|
+
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
8248
|
+
changes.push({ path: (0, import_node_path4.join)(worktree, rel), relativePath: rel, status });
|
|
8249
|
+
}
|
|
8250
|
+
index += 2;
|
|
8325
8251
|
}
|
|
8326
|
-
return
|
|
8252
|
+
return changes;
|
|
8327
8253
|
}
|
|
8328
|
-
function
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
const hooks = {};
|
|
8333
|
-
for (const [event, value] of Object.entries(input2.hooks)) {
|
|
8334
|
-
if (!Array.isArray(value)) {
|
|
8335
|
-
hooks[event] = value;
|
|
8254
|
+
async function applyChangesToOpenViking(config, team, changes, options = {}) {
|
|
8255
|
+
const ov = await openVikingCliForMode(false);
|
|
8256
|
+
for (const change of changes) {
|
|
8257
|
+
if (!change.relativePath.endsWith(".md")) {
|
|
8336
8258
|
continue;
|
|
8337
8259
|
}
|
|
8338
|
-
const
|
|
8339
|
-
if (
|
|
8340
|
-
|
|
8260
|
+
const firstSegment = change.relativePath.split("/")[0];
|
|
8261
|
+
if (!SHAREABLE_MEMORY_KIND_DIRS.includes(firstSegment)) {
|
|
8262
|
+
continue;
|
|
8341
8263
|
}
|
|
8264
|
+
const uri = workfileToVikingUri(config, team, change.path);
|
|
8265
|
+
if (change.status === "removed") {
|
|
8266
|
+
await removeMemoryUri(config, ov, uri, false, options);
|
|
8267
|
+
continue;
|
|
8268
|
+
}
|
|
8269
|
+
if (!await isFile(change.path)) {
|
|
8270
|
+
continue;
|
|
8271
|
+
}
|
|
8272
|
+
const ovHasResource = await vikingResourceExists(ov, config, uri);
|
|
8273
|
+
if (ovHasResource) {
|
|
8274
|
+
const reason = change.status === "modified" ? "overwriting local with upstream (local edits to the shared subtree are not preserved across sync)" : "aligning OV to upstream (resource pre-existed in OV, likely from an earlier local publish or sync)";
|
|
8275
|
+
if (options.quiet !== true) {
|
|
8276
|
+
console.warn(`share sync: ${uri}: ${reason}.`);
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
await ensureSharedDirectoryChain(config, ov, uri, false, options);
|
|
8280
|
+
const writeMode = ovHasResource ? "replace" : "create";
|
|
8281
|
+
await ingestSingleFile(ov, config, uri, change.path, writeMode, options);
|
|
8342
8282
|
}
|
|
8343
|
-
if (Object.keys(hooks).length === 0) {
|
|
8344
|
-
const next = { ...input2 };
|
|
8345
|
-
delete next.hooks;
|
|
8346
|
-
return next;
|
|
8347
|
-
}
|
|
8348
|
-
return { ...input2, hooks };
|
|
8349
|
-
}
|
|
8350
|
-
function isManagedThreadnoteEntry(value) {
|
|
8351
|
-
return isJsonObject(value) && value[THREADNOTE_HOOK_MARKER] === THREADNOTE_HOOK_MARKER_VALUE;
|
|
8352
|
-
}
|
|
8353
|
-
function ensureMutableObject(value) {
|
|
8354
|
-
return isJsonObject(value) ? { ...value } : {};
|
|
8355
8283
|
}
|
|
8356
|
-
|
|
8357
|
-
|
|
8284
|
+
|
|
8285
|
+
// src/memory.ts
|
|
8286
|
+
function parseMemoryKind(value) {
|
|
8287
|
+
if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
|
|
8288
|
+
return value;
|
|
8289
|
+
}
|
|
8290
|
+
throw new Error(`Unsupported memory kind "${value}". Expected durable, handoff, incident, preference, or smoke.`);
|
|
8358
8291
|
}
|
|
8359
|
-
function
|
|
8360
|
-
if (
|
|
8361
|
-
|
|
8362
|
-
return;
|
|
8292
|
+
function parseMemoryStatus(value) {
|
|
8293
|
+
if (["active", "archived", "superseded"].includes(value)) {
|
|
8294
|
+
return value;
|
|
8363
8295
|
}
|
|
8364
|
-
|
|
8365
|
-
[
|
|
8366
|
-
"Codex CLI does not currently expose lifecycle hooks (no PreCompact or SessionStart analog).",
|
|
8367
|
-
"Threadnote already installs Codex user instructions at ~/.codex/AGENTS.md; that remains the active guidance surface.",
|
|
8368
|
-
"If a future Codex release adds hook events, threadnote will pick them up via `install-hooks codex`."
|
|
8369
|
-
].join("\n")
|
|
8370
|
-
);
|
|
8296
|
+
throw new Error(`Unsupported memory status "${value}". Expected active, archived, or superseded.`);
|
|
8371
8297
|
}
|
|
8372
|
-
function
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8298
|
+
async function runRemember(config, options) {
|
|
8299
|
+
const text = await getInputText(options.text, options.stdin === true);
|
|
8300
|
+
if (!text.trim()) {
|
|
8301
|
+
throw new Error("Provide memory text with --text or --stdin.");
|
|
8376
8302
|
}
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8303
|
+
const metadata = {
|
|
8304
|
+
kind: options.kind ?? "durable",
|
|
8305
|
+
project: normalizeOptionalMetadata(options.project),
|
|
8306
|
+
sourceAgentClient: options.sourceAgentClient ?? "codex",
|
|
8307
|
+
status: options.status ?? "active",
|
|
8308
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8309
|
+
topic: normalizeOptionalMetadata(options.topic)
|
|
8310
|
+
};
|
|
8311
|
+
await storeMemory(config, {
|
|
8312
|
+
bodyText: text.trim(),
|
|
8313
|
+
dryRun: options.dryRun === true,
|
|
8314
|
+
metadata,
|
|
8315
|
+
replaceUri: options.replace,
|
|
8316
|
+
title: "MEMORY"
|
|
8317
|
+
});
|
|
8384
8318
|
}
|
|
8385
|
-
async function
|
|
8386
|
-
const
|
|
8387
|
-
|
|
8388
|
-
|
|
8319
|
+
async function runMigrateMemories(config, options) {
|
|
8320
|
+
const dryRun = options.dryRun === true;
|
|
8321
|
+
const limit = options.limit ? parsePositiveInteger(options.limit, "migration limit") : void 0;
|
|
8322
|
+
const sourceAccounts = await legacySourceAccounts(config, options);
|
|
8323
|
+
if (sourceAccounts.length === 0) {
|
|
8324
|
+
console.log("No local OpenViking accounts found to scan.");
|
|
8325
|
+
return;
|
|
8389
8326
|
}
|
|
8390
|
-
const
|
|
8391
|
-
const
|
|
8392
|
-
|
|
8393
|
-
|
|
8327
|
+
const candidates = await legacyMemoryCandidates(config, sourceAccounts);
|
|
8328
|
+
const existingHashes = await existingDurableMemoryHashes(config);
|
|
8329
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
8330
|
+
const migrationPath = (0, import_node_path5.join)(config.agentContextHome, "legacy-memory-migration.txt");
|
|
8331
|
+
let duplicateCount = 0;
|
|
8332
|
+
let migratedCount = 0;
|
|
8333
|
+
let sensitiveCount = 0;
|
|
8334
|
+
if (!dryRun && candidates.length > 0) {
|
|
8335
|
+
await ensureDurableMemoryDirectory(ov, config);
|
|
8394
8336
|
}
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8337
|
+
try {
|
|
8338
|
+
for (const candidate of candidates) {
|
|
8339
|
+
if (existingHashes.has(candidate.hash)) {
|
|
8340
|
+
duplicateCount += 1;
|
|
8341
|
+
continue;
|
|
8342
|
+
}
|
|
8343
|
+
if (existingHashes.has(candidate.comparableHash)) {
|
|
8344
|
+
duplicateCount += 1;
|
|
8345
|
+
continue;
|
|
8346
|
+
}
|
|
8347
|
+
const sensitiveReason = sensitiveMemoryReason(candidate.text);
|
|
8348
|
+
if (sensitiveReason) {
|
|
8349
|
+
sensitiveCount += 1;
|
|
8350
|
+
console.log(
|
|
8351
|
+
`SKIP ${legacySourceLabel(candidate)}: possible ${sensitiveReason}; inspect the source archive manually if needed.`
|
|
8352
|
+
);
|
|
8353
|
+
continue;
|
|
8354
|
+
}
|
|
8355
|
+
if (limit !== void 0 && migratedCount >= limit) {
|
|
8356
|
+
break;
|
|
8357
|
+
}
|
|
8358
|
+
const memoryUri = migratedDurableMemoryUri(config, candidate.hash);
|
|
8359
|
+
if (!dryRun && await vikingResourceExists2(ov, config, memoryUri)) {
|
|
8360
|
+
duplicateCount += 1;
|
|
8361
|
+
existingHashes.add(candidate.hash);
|
|
8362
|
+
continue;
|
|
8363
|
+
}
|
|
8364
|
+
console.log(`${dryRun ? "Would migrate" : "Migrating"} ${legacySourceLabel(candidate)} -> ${memoryUri}`);
|
|
8365
|
+
if (!dryRun) {
|
|
8366
|
+
await (0, import_promises5.writeFile)(migrationPath, candidate.text, { encoding: "utf8", mode: 384 });
|
|
8367
|
+
await (0, import_promises5.chmod)(migrationPath, 384);
|
|
8368
|
+
await writeDurableMemoryFile(ov, config, memoryUri, migrationPath, "create");
|
|
8369
|
+
existingHashes.add(candidate.hash);
|
|
8370
|
+
}
|
|
8371
|
+
migratedCount += 1;
|
|
8398
8372
|
}
|
|
8399
|
-
|
|
8400
|
-
|
|
8373
|
+
} finally {
|
|
8374
|
+
if (!dryRun) {
|
|
8375
|
+
await (0, import_promises5.rm)(migrationPath, { force: true });
|
|
8401
8376
|
}
|
|
8402
8377
|
}
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
nextStep: "Continue from this auto-snapshot. A manual `threadnote handoff` will produce a richer write-up if you have more context.",
|
|
8413
|
-
project,
|
|
8414
|
-
sourceAgentClient: "claude",
|
|
8415
|
-
task: "Auto-snapshot captured at Claude PreCompact (deterministic safety net before context compaction).",
|
|
8416
|
-
tests: "- not recorded (auto-snapshot)",
|
|
8417
|
-
topic: HOOK_AUTO_PRECOMPACT_TOPIC
|
|
8418
|
-
});
|
|
8419
|
-
} catch (err) {
|
|
8420
|
-
process.stderr.write(
|
|
8421
|
-
`threadnote pre-compact-hook: snapshot skipped (${err instanceof Error ? err.message : String(err)})
|
|
8422
|
-
`
|
|
8423
|
-
);
|
|
8424
|
-
}
|
|
8425
|
-
}
|
|
8426
|
-
async function runSessionStartHook(config, options = {}) {
|
|
8427
|
-
try {
|
|
8428
|
-
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
8429
|
-
if (!repoRoot) {
|
|
8430
|
-
return;
|
|
8431
|
-
}
|
|
8432
|
-
const project = (0, import_node_path7.basename)(repoRoot);
|
|
8433
|
-
await emitUpdateBannerIfOutdated(config);
|
|
8434
|
-
process.stdout.write(`## Threadnote \u2014 latest context for ${project}
|
|
8435
|
-
|
|
8436
|
-
`);
|
|
8437
|
-
await runRecall(config, {
|
|
8438
|
-
dryRun: options.dryRun === true,
|
|
8439
|
-
inferScope: true,
|
|
8440
|
-
nodeLimit: "5",
|
|
8441
|
-
query: `${project} latest handoff durable feature memory`
|
|
8442
|
-
});
|
|
8443
|
-
} catch (err) {
|
|
8444
|
-
process.stderr.write(
|
|
8445
|
-
`threadnote session-start-hook: recall skipped (${err instanceof Error ? err.message : String(err)})
|
|
8446
|
-
`
|
|
8447
|
-
);
|
|
8448
|
-
}
|
|
8378
|
+
console.log(
|
|
8379
|
+
[
|
|
8380
|
+
`Migration summary: ${migratedCount} ${dryRun ? "would be migrated" : "migrated"}`,
|
|
8381
|
+
`${duplicateCount} duplicate(s) skipped`,
|
|
8382
|
+
`${sensitiveCount} sensitive-looking item(s) skipped`,
|
|
8383
|
+
`${candidates.length} legacy Threadnote item(s) scanned`,
|
|
8384
|
+
`source account(s): ${sourceAccounts.join(", ")}`
|
|
8385
|
+
].join("; ")
|
|
8386
|
+
);
|
|
8449
8387
|
}
|
|
8450
|
-
async function
|
|
8388
|
+
async function runMigrateLifecycle(config, options) {
|
|
8389
|
+
const dryRun = options.dryRun === true || options.apply !== true;
|
|
8390
|
+
const limit = options.limit ? parsePositiveInteger(options.limit, "lifecycle migration limit") : void 0;
|
|
8391
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
8392
|
+
const candidates = await legacyLifecycleHandoffCandidates(config);
|
|
8393
|
+
const migrationPath = (0, import_node_path5.join)(config.agentContextHome, "lifecycle-memory-migration.txt");
|
|
8394
|
+
let existingCount = 0;
|
|
8395
|
+
let migratedCount = 0;
|
|
8396
|
+
let skippedCount = 0;
|
|
8451
8397
|
try {
|
|
8452
|
-
const
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
`[threadnote] v${result.latestVersion} available (current v${result.currentVersion}). Auto-updating in the background; the new version takes effect next session.
|
|
8462
|
-
|
|
8463
|
-
`
|
|
8398
|
+
for (const candidate of candidates) {
|
|
8399
|
+
if (limit !== void 0 && migratedCount >= limit) {
|
|
8400
|
+
break;
|
|
8401
|
+
}
|
|
8402
|
+
const destinationUri = lifecycleMigrationUri(config, candidate.metadata, sha256(candidate.original.trim()));
|
|
8403
|
+
const migratedMemory = formatMemoryDocument(
|
|
8404
|
+
"HANDOFF",
|
|
8405
|
+
candidate.metadata,
|
|
8406
|
+
["Migrated legacy handoff from the historical events trail.", "", candidate.original.trim()].join("\n")
|
|
8464
8407
|
);
|
|
8465
|
-
|
|
8466
|
-
|
|
8408
|
+
console.log(`${dryRun ? "Would migrate" : "Migrating"} ${candidate.sourceUri} -> ${destinationUri}`);
|
|
8409
|
+
if (!dryRun) {
|
|
8410
|
+
if (await vikingResourceExists2(ov, config, destinationUri)) {
|
|
8411
|
+
existingCount += 1;
|
|
8412
|
+
console.log(`Archived copy already exists; cleaning up legacy source: ${candidate.sourceUri}`);
|
|
8413
|
+
} else {
|
|
8414
|
+
await (0, import_promises5.writeFile)(migrationPath, migratedMemory, { encoding: "utf8", mode: 384 });
|
|
8415
|
+
await (0, import_promises5.chmod)(migrationPath, 384);
|
|
8416
|
+
await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, candidate.metadata));
|
|
8417
|
+
await writeDurableMemoryFile(ov, config, destinationUri, migrationPath, "create");
|
|
8418
|
+
}
|
|
8419
|
+
const removedOriginal = await removeVikingResourceWithRetry(ov, config, candidate.sourceUri);
|
|
8420
|
+
if (!removedOriginal) {
|
|
8421
|
+
console.error(
|
|
8422
|
+
`Migrated copy stored, but original is still processing. Retry later: threadnote forget ${candidate.sourceUri}`
|
|
8423
|
+
);
|
|
8424
|
+
skippedCount += 1;
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
migratedCount += 1;
|
|
8428
|
+
}
|
|
8429
|
+
} finally {
|
|
8430
|
+
if (!dryRun) {
|
|
8431
|
+
await (0, import_promises5.rm)(migrationPath, { force: true });
|
|
8467
8432
|
}
|
|
8468
|
-
process.stdout.write(
|
|
8469
|
-
`[threadnote] v${result.latestVersion} available (current v${result.currentVersion}). Run: threadnote update
|
|
8470
|
-
|
|
8471
|
-
`
|
|
8472
|
-
);
|
|
8473
|
-
} catch {
|
|
8474
8433
|
}
|
|
8434
|
+
console.log(
|
|
8435
|
+
[
|
|
8436
|
+
`Lifecycle migration summary: ${migratedCount} clear legacy handoff(s) ${dryRun ? "would be migrated" : "migrated"}`,
|
|
8437
|
+
`${existingCount} existing archived copy/copies reused`,
|
|
8438
|
+
`${skippedCount} legacy source(s) still processing`,
|
|
8439
|
+
`${candidates.length} clear legacy handoff candidate(s) found`,
|
|
8440
|
+
dryRun ? "Run with --apply to perform this migration." : void 0
|
|
8441
|
+
].filter((part) => part !== void 0).join("; ")
|
|
8442
|
+
);
|
|
8475
8443
|
}
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
async function runSeed(config, options) {
|
|
8481
|
-
const manifest = await readSeedManifest(config.manifestPath);
|
|
8482
|
-
const ignorePatterns = await loadIgnorePatterns();
|
|
8444
|
+
async function runRecall(config, options) {
|
|
8445
|
+
if (options.dryRun !== true) {
|
|
8446
|
+
await syncSharedReposAndLog(config);
|
|
8447
|
+
}
|
|
8483
8448
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
console.log(`WARN project missing: ${project.name} (${projectRoot})`);
|
|
8490
|
-
continue;
|
|
8491
|
-
}
|
|
8492
|
-
const candidates = await collectSeedCandidates(project, projectRoot, ignorePatterns);
|
|
8493
|
-
for (const candidate of candidates) {
|
|
8494
|
-
const importPath = await prepareSeedFile(config, candidate, options.dryRun === true);
|
|
8495
|
-
if (!importPath) {
|
|
8496
|
-
skippedCount += 1;
|
|
8497
|
-
continue;
|
|
8498
|
-
}
|
|
8499
|
-
const args = withIdentity(config, ["add-resource", importPath, "--to", candidate.destinationUri, "--wait"]);
|
|
8500
|
-
await maybeRun(options.dryRun === true, ov, args);
|
|
8501
|
-
importedCount += 1;
|
|
8502
|
-
}
|
|
8449
|
+
const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, options.query));
|
|
8450
|
+
const args = ["search", options.query];
|
|
8451
|
+
if (inferredUri) {
|
|
8452
|
+
args.push("--uri", inferredUri);
|
|
8453
|
+
console.log(`Recall scope: ${inferredUri}`);
|
|
8503
8454
|
}
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
async function runInitManifest(config, options) {
|
|
8507
|
-
const manifestPath = expandPath(
|
|
8508
|
-
options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path8.join)(config.agentContextHome, USER_MANIFEST_NAME)
|
|
8509
|
-
);
|
|
8510
|
-
const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
|
|
8511
|
-
const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
|
|
8512
|
-
const existingProjects = existingManifest?.projects ?? [];
|
|
8513
|
-
const projects = [...existingProjects];
|
|
8514
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8515
|
-
for (const project of existingProjects) {
|
|
8516
|
-
seen.add(await projectIdentity(project.path));
|
|
8455
|
+
if (options.nodeLimit) {
|
|
8456
|
+
args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
|
|
8517
8457
|
}
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8458
|
+
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
8459
|
+
await augmentRecallWithSeededResources(config, ov, options, inferredUri);
|
|
8460
|
+
await printExactMemoryMatches(config, ov, options.query, {
|
|
8461
|
+
dryRun: options.dryRun === true,
|
|
8462
|
+
includeArchived: options.includeArchived === true
|
|
8463
|
+
});
|
|
8464
|
+
}
|
|
8465
|
+
async function augmentRecallWithSeededResources(config, ov, options, inferredUri) {
|
|
8466
|
+
if (options.uri || options.inferScope === false) {
|
|
8467
|
+
return;
|
|
8527
8468
|
}
|
|
8528
|
-
const
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
name: project.name,
|
|
8532
|
-
path: project.path,
|
|
8533
|
-
uri: project.uri,
|
|
8534
|
-
seed: [...project.seed]
|
|
8535
|
-
}))
|
|
8536
|
-
};
|
|
8537
|
-
if (existingManifest?.futureMonorepo) {
|
|
8538
|
-
const futureMonorepoKey = "future_monorepo";
|
|
8539
|
-
const pathCandidatesKey = "path_candidates";
|
|
8540
|
-
outputManifest[futureMonorepoKey] = {
|
|
8541
|
-
[pathCandidatesKey]: [...existingManifest.futureMonorepo.pathCandidates],
|
|
8542
|
-
uri: existingManifest.futureMonorepo.uri
|
|
8543
|
-
};
|
|
8469
|
+
const project = await inferProjectFromQuery(config.manifestPath, options.query);
|
|
8470
|
+
if (!project) {
|
|
8471
|
+
return;
|
|
8544
8472
|
}
|
|
8545
|
-
const
|
|
8546
|
-
if (
|
|
8547
|
-
console.log(`# Would write ${manifestPath}`);
|
|
8548
|
-
console.log(output2.trimEnd());
|
|
8473
|
+
const projectResourceUri = trimTrailingSlash(project.uri);
|
|
8474
|
+
if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
|
|
8549
8475
|
return;
|
|
8550
8476
|
}
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
console.log(`Wrote manifest: ${manifestPath}`);
|
|
8555
|
-
console.log("Seed with:");
|
|
8556
|
-
console.log(" threadnote seed --dry-run");
|
|
8557
|
-
console.log(" threadnote seed");
|
|
8558
|
-
}
|
|
8559
|
-
async function runSeedSkills(config, options) {
|
|
8560
|
-
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8561
|
-
const skills = await collectSkillCandidates(config);
|
|
8562
|
-
const nativeMode = options.native === true;
|
|
8563
|
-
console.log(
|
|
8564
|
-
nativeMode ? "Skill seed mode: native OpenViking skills. This requires a working VLM config." : "Skill seed mode: resource catalog. Use --native only after configuring a working VLM provider."
|
|
8565
|
-
);
|
|
8566
|
-
for (const skill of skills) {
|
|
8567
|
-
console.log(`Skill ${skill.source}: ${skill.filePath}`);
|
|
8568
|
-
const args = nativeMode ? ["add-skill", skill.filePath, "--wait"] : ["add-resource", skill.filePath, "--to", skillResourceUri(skill), "--wait"];
|
|
8569
|
-
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
8477
|
+
const args = ["search", options.query, "--uri", projectResourceUri];
|
|
8478
|
+
if (options.nodeLimit) {
|
|
8479
|
+
args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
|
|
8570
8480
|
}
|
|
8571
|
-
console.log(`
|
|
8481
|
+
console.log(`
|
|
8482
|
+
Also searching seeded resources: ${projectResourceUri}`);
|
|
8483
|
+
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
8572
8484
|
}
|
|
8573
|
-
async function
|
|
8574
|
-
|
|
8575
|
-
if (
|
|
8576
|
-
|
|
8485
|
+
async function runRead(config, uri, options) {
|
|
8486
|
+
assertVikingUri(uri);
|
|
8487
|
+
if (options.dryRun !== true) {
|
|
8488
|
+
await syncSharedReposAndLog(config);
|
|
8489
|
+
}
|
|
8490
|
+
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8491
|
+
const result = await maybeRun(options.dryRun === true, ov, withIdentity(config, ["read", uri]));
|
|
8492
|
+
if (result && result.stdout.includes("[Directory overview is not ready]") && (uri.endsWith("/.overview.md") || uri.endsWith("/.abstract.md"))) {
|
|
8493
|
+
const parentUri2 = parentVikingUri(uri);
|
|
8494
|
+
console.log("\nThis is a generated summary placeholder. To read the underlying content, inspect leaf nodes:");
|
|
8495
|
+
console.log(` threadnote list ${parentUri2} --all --recursive`);
|
|
8577
8496
|
}
|
|
8578
|
-
return await gitValue(["rev-parse", "--show-toplevel"], inputPath) ?? inputPath;
|
|
8579
8497
|
}
|
|
8580
|
-
async function
|
|
8581
|
-
const expanded = expandPath(path);
|
|
8498
|
+
async function syncSharedReposAndLog(config) {
|
|
8582
8499
|
try {
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8500
|
+
const syncResult = await syncSharedReposBeforeAgentRead(config);
|
|
8501
|
+
if (syncResult.syncedTeams.length > 0) {
|
|
8502
|
+
console.error(`Auto-synced shared memories: ${syncResult.syncedTeams.join(", ")}`);
|
|
8503
|
+
}
|
|
8504
|
+
for (const warning of syncResult.warnings) {
|
|
8505
|
+
console.error(`Auto-sync warning: ${warning}`);
|
|
8506
|
+
}
|
|
8507
|
+
} catch (err) {
|
|
8508
|
+
console.error(`Auto-sync warning: ${err instanceof Error ? err.message : String(err)}`);
|
|
8586
8509
|
}
|
|
8587
8510
|
}
|
|
8588
|
-
function
|
|
8589
|
-
|
|
8590
|
-
const
|
|
8591
|
-
const
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
if (usedNames.has(name) || usedUris.has(uri)) {
|
|
8595
|
-
name = `${baseName}-${sha256(repoRoot).slice(0, 8)}`;
|
|
8596
|
-
uri = `viking://resources/repos/${name}`;
|
|
8597
|
-
}
|
|
8598
|
-
return {
|
|
8599
|
-
name,
|
|
8600
|
-
path: portablePath(repoRoot),
|
|
8601
|
-
seed: DEFAULT_SEED_PATTERNS,
|
|
8602
|
-
uri
|
|
8603
|
-
};
|
|
8604
|
-
}
|
|
8605
|
-
async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
|
|
8606
|
-
const candidates = [];
|
|
8607
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8608
|
-
for (const pattern of project.seed) {
|
|
8609
|
-
const files = await resolveProjectPattern(projectRoot, pattern);
|
|
8610
|
-
for (const filePath of files) {
|
|
8611
|
-
const relativePath = toPosixPath((0, import_node_path8.relative)(projectRoot, filePath));
|
|
8612
|
-
if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
|
|
8613
|
-
continue;
|
|
8614
|
-
}
|
|
8615
|
-
seen.add(relativePath);
|
|
8616
|
-
candidates.push({
|
|
8617
|
-
destinationUri: `${trimTrailingSlash(project.uri)}/${relativePath}`,
|
|
8618
|
-
filePath,
|
|
8619
|
-
projectName: project.name,
|
|
8620
|
-
relativePath
|
|
8621
|
-
});
|
|
8622
|
-
}
|
|
8623
|
-
}
|
|
8624
|
-
return candidates;
|
|
8625
|
-
}
|
|
8626
|
-
async function resolveProjectPattern(projectRoot, pattern) {
|
|
8627
|
-
const normalizedPattern = toPosixPath(pattern);
|
|
8628
|
-
if (!hasGlob(normalizedPattern)) {
|
|
8629
|
-
const filePath = (0, import_node_path8.join)(projectRoot, normalizedPattern);
|
|
8630
|
-
return await isFile(filePath) ? [filePath] : [];
|
|
8631
|
-
}
|
|
8632
|
-
const globBase = getGlobBase(normalizedPattern);
|
|
8633
|
-
const basePath = (0, import_node_path8.join)(projectRoot, globBase);
|
|
8634
|
-
if (!await exists(basePath)) {
|
|
8635
|
-
return [];
|
|
8511
|
+
async function runList(config, uri, options) {
|
|
8512
|
+
assertVikingUri(uri);
|
|
8513
|
+
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8514
|
+
const args = ["ls", uri];
|
|
8515
|
+
if (options.all === true) {
|
|
8516
|
+
args.push("--all");
|
|
8636
8517
|
}
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path8.relative)(projectRoot, filePath))));
|
|
8640
|
-
}
|
|
8641
|
-
async function prepareSeedFile(config, candidate, dryRun) {
|
|
8642
|
-
const content = await (0, import_promises7.readFile)(candidate.filePath, "utf8");
|
|
8643
|
-
const redactedContent = shouldRedactPath(candidate.relativePath) ? redactContent(candidate.relativePath, content) : content;
|
|
8644
|
-
const secretMatches = detectSecretMatches(redactedContent);
|
|
8645
|
-
if (secretMatches.length > 0) {
|
|
8646
|
-
console.log(
|
|
8647
|
-
`SKIP ${candidate.projectName}/${candidate.relativePath}: possible secret (${secretMatches.slice(0, MAX_SECRET_MATCHES_TO_PRINT).join(", ")})`
|
|
8648
|
-
);
|
|
8649
|
-
return void 0;
|
|
8518
|
+
if (options.recursive === true) {
|
|
8519
|
+
args.push("--recursive");
|
|
8650
8520
|
}
|
|
8651
|
-
if (
|
|
8652
|
-
|
|
8521
|
+
if (options.simple === true) {
|
|
8522
|
+
args.push("--simple");
|
|
8653
8523
|
}
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
console.log(`Would write redacted copy: ${redactedPath}`);
|
|
8657
|
-
return redactedPath;
|
|
8524
|
+
if (options.nodeLimit) {
|
|
8525
|
+
args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
|
|
8658
8526
|
}
|
|
8659
|
-
await
|
|
8660
|
-
await (0, import_promises7.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
|
|
8661
|
-
await (0, import_promises7.chmod)(redactedPath, 384);
|
|
8662
|
-
return redactedPath;
|
|
8527
|
+
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
8663
8528
|
}
|
|
8664
|
-
async function
|
|
8665
|
-
const
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8529
|
+
async function runHandoff(config, options) {
|
|
8530
|
+
const { bodyText, metadata } = await buildHandoff(options);
|
|
8531
|
+
await storeMemory(config, {
|
|
8532
|
+
bodyText,
|
|
8533
|
+
dryRun: options.dryRun === true,
|
|
8534
|
+
metadata,
|
|
8535
|
+
replaceUri: options.replace,
|
|
8536
|
+
title: "HANDOFF"
|
|
8537
|
+
});
|
|
8538
|
+
}
|
|
8539
|
+
async function runArchive(config, uri, options) {
|
|
8540
|
+
assertVikingUri(uri);
|
|
8541
|
+
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8542
|
+
const readResult = await maybeRun(options.dryRun === true, ov, withIdentity(config, ["read", uri]));
|
|
8543
|
+
const original = readResult?.stdout.trim();
|
|
8544
|
+
if (options.dryRun === true) {
|
|
8545
|
+
const fallbackMetadata = {
|
|
8546
|
+
archivedFrom: uri,
|
|
8547
|
+
kind: options.kind ?? "handoff",
|
|
8548
|
+
project: normalizeOptionalMetadata(options.project),
|
|
8549
|
+
sourceAgentClient: "threadnote",
|
|
8550
|
+
status: "archived",
|
|
8551
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8552
|
+
topic: normalizeOptionalMetadata(options.topic)
|
|
8553
|
+
};
|
|
8554
|
+
await storeMemory(config, {
|
|
8555
|
+
bodyText: ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n"),
|
|
8556
|
+
dryRun: true,
|
|
8557
|
+
metadata: fallbackMetadata,
|
|
8558
|
+
title: "MEMORY"
|
|
8559
|
+
});
|
|
8560
|
+
console.log(formatShellCommand(ov, withIdentity(config, ["rm", uri])));
|
|
8561
|
+
return;
|
|
8680
8562
|
}
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8563
|
+
if (!original) {
|
|
8564
|
+
throw new Error(`Could not read ${uri} before archiving.`);
|
|
8565
|
+
}
|
|
8566
|
+
const inferredMetadata = inferMemoryMetadata(original);
|
|
8567
|
+
const metadata = {
|
|
8568
|
+
archivedFrom: uri,
|
|
8569
|
+
kind: options.kind ?? inferredMetadata.kind ?? "handoff",
|
|
8570
|
+
project: normalizeOptionalMetadata(options.project) ?? inferredMetadata.project,
|
|
8571
|
+
sourceAgentClient: "threadnote",
|
|
8572
|
+
status: "archived",
|
|
8573
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8574
|
+
topic: normalizeOptionalMetadata(options.topic) ?? inferredMetadata.topic
|
|
8575
|
+
};
|
|
8576
|
+
await storeMemory(config, {
|
|
8577
|
+
bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
|
|
8578
|
+
dryRun: false,
|
|
8579
|
+
metadata,
|
|
8580
|
+
title: "MEMORY"
|
|
8581
|
+
});
|
|
8582
|
+
const removedOriginal = await removeVikingResourceWithRetry(ov, config, uri);
|
|
8583
|
+
if (removedOriginal) {
|
|
8584
|
+
console.log(`Archived original memory: ${uri}`);
|
|
8585
|
+
} else {
|
|
8586
|
+
console.error(`Archive stored, but original memory is still processing. Retry later: threadnote forget ${uri}`);
|
|
8699
8587
|
}
|
|
8700
|
-
return skills;
|
|
8701
8588
|
}
|
|
8702
|
-
async function
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8589
|
+
async function runForget(config, uri, options) {
|
|
8590
|
+
assertVikingUri(uri);
|
|
8591
|
+
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8592
|
+
if (options.dryRun === true) {
|
|
8593
|
+
await maybeRun(true, ov, withIdentity(config, ["rm", uri]));
|
|
8594
|
+
return;
|
|
8706
8595
|
}
|
|
8707
|
-
const
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
return [];
|
|
8596
|
+
const removed = await removeVikingResourceWithRetry(ov, config, uri);
|
|
8597
|
+
if (!removed) {
|
|
8598
|
+
throw new Error(`Resource is still being processed; retry later: threadnote forget ${uri}`);
|
|
8711
8599
|
}
|
|
8712
|
-
const regex = globToRegExp(normalizedPattern);
|
|
8713
|
-
const files = await walkFiles(basePath);
|
|
8714
|
-
return files.filter((filePath) => regex.test(toPosixPath(filePath)));
|
|
8715
8600
|
}
|
|
8716
|
-
function
|
|
8717
|
-
|
|
8718
|
-
}
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
8601
|
+
async function runExportPack(config, options) {
|
|
8602
|
+
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8603
|
+
const defaultPath = (0, import_node_path5.join)(config.agentContextHome, `threadnote-${safeTimestamp()}.ovpack`);
|
|
8604
|
+
const outputPath = expandPath(options.path ?? defaultPath);
|
|
8605
|
+
await maybeRun(options.dryRun === true, ov, withIdentity(config, ["export", options.uri ?? "viking://", outputPath]));
|
|
8722
8606
|
}
|
|
8723
|
-
function
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
const normalizedPattern = toPosixPath(pattern);
|
|
8727
|
-
if (normalizedPattern.endsWith("/")) {
|
|
8728
|
-
const directory = normalizedPattern.slice(0, -1);
|
|
8729
|
-
if (path === directory || path.includes(`/${directory}/`) || path.startsWith(`${directory}/`)) {
|
|
8730
|
-
return true;
|
|
8731
|
-
}
|
|
8732
|
-
continue;
|
|
8733
|
-
}
|
|
8734
|
-
if (globToRegExp(normalizedPattern).test(path) || globToRegExp(`**/${normalizedPattern}`).test(path)) {
|
|
8735
|
-
return true;
|
|
8736
|
-
}
|
|
8607
|
+
async function runImportPack(config, options) {
|
|
8608
|
+
if (!options.path) {
|
|
8609
|
+
throw new Error("Provide --path for import-pack.");
|
|
8737
8610
|
}
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8611
|
+
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8612
|
+
await maybeRun(
|
|
8613
|
+
options.dryRun === true,
|
|
8614
|
+
ov,
|
|
8615
|
+
withIdentity(config, ["import", expandPath(options.path), options.targetUri ?? "viking://"])
|
|
8616
|
+
);
|
|
8743
8617
|
}
|
|
8744
|
-
function
|
|
8745
|
-
if (
|
|
8746
|
-
|
|
8747
|
-
const parsed = JSON.parse(content);
|
|
8748
|
-
return `${JSON.stringify(redactJsonValue(parsed), null, 2)}
|
|
8749
|
-
`;
|
|
8750
|
-
} catch (_err) {
|
|
8751
|
-
return redactText(content);
|
|
8752
|
-
}
|
|
8618
|
+
async function inferRecallUri(config, query) {
|
|
8619
|
+
if (!/\bskills?\b/.test(query.toLowerCase())) {
|
|
8620
|
+
return void 0;
|
|
8753
8621
|
}
|
|
8754
|
-
|
|
8622
|
+
const project = await inferProjectFromQuery(config.manifestPath, query);
|
|
8623
|
+
return project ? `viking://resources/agent-skills/repo-local-${uriSegment(project.name)}` : "viking://resources/agent-skills";
|
|
8755
8624
|
}
|
|
8756
|
-
function
|
|
8757
|
-
|
|
8758
|
-
|
|
8625
|
+
async function printExactMemoryMatches(config, ov, query, options) {
|
|
8626
|
+
const terms = exactRecallTerms(query);
|
|
8627
|
+
if (terms.length === 0) {
|
|
8628
|
+
return;
|
|
8759
8629
|
}
|
|
8760
|
-
|
|
8761
|
-
|
|
8630
|
+
const scopes = exactMemoryScopes(config, options.includeArchived);
|
|
8631
|
+
const outputs = [];
|
|
8632
|
+
for (const term of terms) {
|
|
8633
|
+
for (const scope of scopes) {
|
|
8634
|
+
const args = withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5"]);
|
|
8635
|
+
if (options.dryRun) {
|
|
8636
|
+
outputs.push(formatShellCommand(ov, args));
|
|
8637
|
+
continue;
|
|
8638
|
+
}
|
|
8639
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
8640
|
+
const output2 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
8641
|
+
if (result.exitCode === 0 && grepOutputHasMatches(output2)) {
|
|
8642
|
+
outputs.push(output2);
|
|
8643
|
+
}
|
|
8644
|
+
}
|
|
8762
8645
|
}
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
output2[key] = isSensitiveKey(key) ? "[REDACTED]" : redactJsonValue(nestedValue);
|
|
8646
|
+
if (outputs.length === 0) {
|
|
8647
|
+
return;
|
|
8766
8648
|
}
|
|
8767
|
-
|
|
8649
|
+
console.log("\nExact durable memory matches:");
|
|
8650
|
+
console.log(outputs.join("\n\n"));
|
|
8768
8651
|
}
|
|
8769
|
-
function
|
|
8770
|
-
|
|
8652
|
+
async function storeMemory(config, options) {
|
|
8653
|
+
if (options.replaceUri) {
|
|
8654
|
+
assertVikingUri(options.replaceUri);
|
|
8655
|
+
}
|
|
8656
|
+
const ov = await openVikingCliForMode(options.dryRun);
|
|
8657
|
+
const memoryPath = (0, import_node_path5.join)(config.agentContextHome, "last-memory.txt");
|
|
8658
|
+
const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
|
|
8659
|
+
const candidateMemory = formatMemoryDocument(options.title, candidateMetadata, options.bodyText);
|
|
8660
|
+
const memoryUri = memoryUriFor(config, candidateMemory, candidateMetadata);
|
|
8661
|
+
const isInPlaceUpdate = options.replaceUri !== void 0 && options.replaceUri === memoryUri;
|
|
8662
|
+
const finalMetadata = isInPlaceUpdate ? { ...options.metadata, supersedes: void 0 } : candidateMetadata;
|
|
8663
|
+
const memory = isInPlaceUpdate ? formatMemoryDocument(options.title, finalMetadata, options.bodyText) : candidateMemory;
|
|
8664
|
+
const writeMode = await memoryWriteMode(ov, config, memoryUri, finalMetadata);
|
|
8665
|
+
if (options.dryRun) {
|
|
8666
|
+
console.log(memory);
|
|
8667
|
+
console.log("\nWould run:");
|
|
8668
|
+
console.log(
|
|
8669
|
+
formatShellCommand(
|
|
8670
|
+
ov,
|
|
8671
|
+
withIdentity(config, [
|
|
8672
|
+
"write",
|
|
8673
|
+
memoryUri,
|
|
8674
|
+
"--from-file",
|
|
8675
|
+
memoryPath,
|
|
8676
|
+
"--mode",
|
|
8677
|
+
writeMode,
|
|
8678
|
+
"--wait",
|
|
8679
|
+
"--timeout",
|
|
8680
|
+
"120"
|
|
8681
|
+
])
|
|
8682
|
+
)
|
|
8683
|
+
);
|
|
8684
|
+
if (options.replaceUri && !isInPlaceUpdate) {
|
|
8685
|
+
console.log(formatShellCommand(ov, withIdentity(config, ["rm", options.replaceUri])));
|
|
8686
|
+
}
|
|
8687
|
+
return;
|
|
8688
|
+
}
|
|
8689
|
+
await (0, import_promises5.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
|
|
8690
|
+
await (0, import_promises5.chmod)(memoryPath, 384);
|
|
8691
|
+
await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, finalMetadata));
|
|
8692
|
+
await writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode);
|
|
8693
|
+
console.log(`Stored memory: ${memoryUri}`);
|
|
8694
|
+
if (options.replaceUri && !isInPlaceUpdate) {
|
|
8695
|
+
const removedReplacedMemory = await removeVikingResourceWithRetry(ov, config, options.replaceUri);
|
|
8696
|
+
if (removedReplacedMemory) {
|
|
8697
|
+
console.log(`Forgot replaced memory: ${options.replaceUri}`);
|
|
8698
|
+
} else {
|
|
8699
|
+
console.error(
|
|
8700
|
+
`Replacement stored, but the superseded memory is still processing. Retry later: threadnote forget ${options.replaceUri}`
|
|
8701
|
+
);
|
|
8702
|
+
}
|
|
8703
|
+
} else if (isInPlaceUpdate) {
|
|
8704
|
+
console.log(`Updated existing memory in place: ${memoryUri}`);
|
|
8705
|
+
}
|
|
8771
8706
|
}
|
|
8772
|
-
function
|
|
8773
|
-
const
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8707
|
+
async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode) {
|
|
8708
|
+
const args = withIdentity(config, [
|
|
8709
|
+
"write",
|
|
8710
|
+
memoryUri,
|
|
8711
|
+
"--from-file",
|
|
8712
|
+
memoryPath,
|
|
8713
|
+
"--mode",
|
|
8714
|
+
writeMode,
|
|
8715
|
+
"--wait",
|
|
8716
|
+
"--timeout",
|
|
8717
|
+
"120"
|
|
8718
|
+
]);
|
|
8719
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
8720
|
+
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
8721
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
8722
|
+
if (result.exitCode === 0) {
|
|
8723
|
+
if (result.stdout.trim()) {
|
|
8724
|
+
console.log(result.stdout.trim());
|
|
8725
|
+
}
|
|
8726
|
+
if (result.stderr.trim()) {
|
|
8727
|
+
console.error(result.stderr.trim());
|
|
8728
|
+
}
|
|
8729
|
+
return;
|
|
8730
|
+
}
|
|
8731
|
+
if (await vikingResourceExists2(ov, config, memoryUri)) {
|
|
8732
|
+
console.log("OpenViking accepted the memory but returned before the wait completed; waiting for indexing.");
|
|
8733
|
+
await waitForOpenVikingQueue(ov, config);
|
|
8734
|
+
return;
|
|
8785
8735
|
}
|
|
8736
|
+
if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
|
|
8737
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
8738
|
+
}
|
|
8739
|
+
await sleep(1e3 * (attempt + 1));
|
|
8786
8740
|
}
|
|
8787
|
-
return matches;
|
|
8788
8741
|
}
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
];
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8742
|
+
async function waitForOpenVikingQueue(ov, config) {
|
|
8743
|
+
const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
|
|
8744
|
+
if (result.stdout.trim()) {
|
|
8745
|
+
console.log(result.stdout.trim());
|
|
8746
|
+
}
|
|
8747
|
+
if (result.stderr.trim()) {
|
|
8748
|
+
console.error(result.stderr.trim());
|
|
8749
|
+
}
|
|
8750
|
+
}
|
|
8751
|
+
async function removeVikingResourceWithRetry(ov, config, uri) {
|
|
8752
|
+
const args = withIdentity(config, ["rm", uri]);
|
|
8753
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
8754
|
+
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
8755
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
8756
|
+
if (result.exitCode === 0) {
|
|
8757
|
+
if (result.stdout.trim()) {
|
|
8758
|
+
console.log(result.stdout.trim());
|
|
8759
|
+
}
|
|
8760
|
+
if (result.stderr.trim()) {
|
|
8761
|
+
console.error(result.stderr.trim());
|
|
8762
|
+
}
|
|
8763
|
+
return true;
|
|
8764
|
+
}
|
|
8765
|
+
if (isResourceBusy(result.stderr, result.stdout) && attempt === 3) {
|
|
8766
|
+
return false;
|
|
8767
|
+
}
|
|
8768
|
+
if (!isResourceBusy(result.stderr, result.stdout)) {
|
|
8769
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
8770
|
+
}
|
|
8771
|
+
await sleep(1e3 * (attempt + 1));
|
|
8772
|
+
}
|
|
8773
|
+
return false;
|
|
8774
|
+
}
|
|
8775
|
+
async function vikingResourceExists2(ov, config, uri) {
|
|
8776
|
+
const stat4 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
8777
|
+
return stat4.exitCode === 0;
|
|
8778
|
+
}
|
|
8779
|
+
async function ensureDurableMemoryDirectory(ov, config) {
|
|
8780
|
+
await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
|
|
8781
|
+
}
|
|
8782
|
+
async function ensureMemoryDirectory(ov, config, directoryUri) {
|
|
8783
|
+
for (const uri of vikingDirectoryChain(directoryUri)) {
|
|
8784
|
+
const statResult = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
8785
|
+
if (statResult.exitCode === 0) {
|
|
8829
8786
|
continue;
|
|
8830
8787
|
}
|
|
8831
|
-
|
|
8832
|
-
|
|
8788
|
+
await maybeRun(
|
|
8789
|
+
false,
|
|
8790
|
+
ov,
|
|
8791
|
+
withIdentity(config, ["mkdir", uri, "--description", "Threadnote lifecycle-aware local memories."])
|
|
8792
|
+
);
|
|
8793
|
+
}
|
|
8794
|
+
}
|
|
8795
|
+
function durableMemoryDirectoryUri(config) {
|
|
8796
|
+
return `viking://user/${uriSegment(config.user)}/memories/events`;
|
|
8797
|
+
}
|
|
8798
|
+
function migratedDurableMemoryUri(config, hash) {
|
|
8799
|
+
return `${durableMemoryDirectoryUri(config)}/threadnote-migrated-${hash.slice(0, 16)}.md`;
|
|
8800
|
+
}
|
|
8801
|
+
async function hasLegacyLifecycleHandoffCandidates(config) {
|
|
8802
|
+
return (await legacyLifecycleHandoffCandidates(config, 1)).length > 0;
|
|
8803
|
+
}
|
|
8804
|
+
async function legacyLifecycleHandoffCandidates(config, limit) {
|
|
8805
|
+
const eventsRoot = (0, import_node_path5.join)(localUserMemoriesRoot(config), "events");
|
|
8806
|
+
let entries;
|
|
8807
|
+
try {
|
|
8808
|
+
entries = await (0, import_promises5.readdir)(eventsRoot, { withFileTypes: true });
|
|
8809
|
+
} catch (_err) {
|
|
8810
|
+
return [];
|
|
8811
|
+
}
|
|
8812
|
+
const candidates = [];
|
|
8813
|
+
for (const entry of entries) {
|
|
8814
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
|
|
8815
|
+
continue;
|
|
8816
|
+
}
|
|
8817
|
+
const sourcePath = (0, import_node_path5.join)(eventsRoot, entry.name);
|
|
8818
|
+
const original = await readTextIfExists(sourcePath);
|
|
8819
|
+
if (!original || !isClearLegacyHandoffMemory(original) || sensitiveMemoryReason(original)) {
|
|
8820
|
+
continue;
|
|
8821
|
+
}
|
|
8822
|
+
const sourceUri = `${durableMemoryDirectoryUri(config)}/${entry.name}`;
|
|
8823
|
+
candidates.push({
|
|
8824
|
+
metadata: {
|
|
8825
|
+
archivedFrom: sourceUri,
|
|
8826
|
+
kind: "handoff",
|
|
8827
|
+
project: inferLegacyProject(original),
|
|
8828
|
+
sourceAgentClient: "threadnote",
|
|
8829
|
+
status: "archived",
|
|
8830
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8831
|
+
},
|
|
8832
|
+
original,
|
|
8833
|
+
sourceUri
|
|
8834
|
+
});
|
|
8835
|
+
if (limit !== void 0 && candidates.length >= limit) {
|
|
8836
|
+
break;
|
|
8833
8837
|
}
|
|
8834
|
-
const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`;
|
|
8835
|
-
const globalRegex = new RegExp(pattern.regex.source, flags);
|
|
8836
|
-
const matches = cleaned.match(globalRegex) ?? [];
|
|
8837
|
-
cleaned = cleaned.replace(globalRegex, pattern.placeholder);
|
|
8838
|
-
redactions.push({ count: matches.length, name: pattern.name });
|
|
8839
8838
|
}
|
|
8840
|
-
return
|
|
8839
|
+
return candidates;
|
|
8841
8840
|
}
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8841
|
+
function lifecycleMigrationUri(config, metadata, hash) {
|
|
8842
|
+
return `${memoryDirectoryUri(config, metadata)}/legacy-${hash.slice(0, 16)}.md`;
|
|
8843
|
+
}
|
|
8844
|
+
function exactMemoryScopes(config, includeArchived) {
|
|
8845
|
+
const userBase = `viking://user/${uriSegment(config.user)}/memories`;
|
|
8846
|
+
const scopes = [
|
|
8847
|
+
`${userBase}/preferences`,
|
|
8848
|
+
`${userBase}/durable/projects`,
|
|
8849
|
+
`${userBase}/handoffs/active`,
|
|
8850
|
+
`${userBase}/incidents/active`,
|
|
8851
|
+
`${userBase}/events`,
|
|
8852
|
+
`${userBase}/shared`,
|
|
8853
|
+
`viking://agent/${uriSegment(config.agentId)}/memories`,
|
|
8854
|
+
// Seeded project resources (READMEs, AGENTS.md, SKILL.md, docs/**) live
|
|
8855
|
+
// under viking://resources/repos/<project>. Include them so an exact-term
|
|
8856
|
+
// grep in a recall surfaces matches in seeded guidance, not only memories.
|
|
8857
|
+
"viking://resources/repos"
|
|
8858
|
+
];
|
|
8859
|
+
return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
|
|
8860
|
+
}
|
|
8861
|
+
function memoryUriFor(config, memory, metadata) {
|
|
8862
|
+
const filename = shouldUseStableMemoryUri(metadata) ? `${uriSegment(metadata.topic ?? "current")}.md` : `threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
|
|
8863
|
+
return `${memoryDirectoryUri(config, metadata)}/${filename}`;
|
|
8864
|
+
}
|
|
8865
|
+
function memoryDirectoryUri(config, metadata) {
|
|
8866
|
+
const baseUri = `viking://user/${uriSegment(config.user)}/memories`;
|
|
8867
|
+
const projectSegment = uriSegment(metadata.project ?? "general");
|
|
8868
|
+
switch (metadata.kind) {
|
|
8869
|
+
case "preference":
|
|
8870
|
+
return metadata.status === "active" ? `${baseUri}/preferences` : `${baseUri}/preferences/${uriSegment(metadata.status)}`;
|
|
8871
|
+
case "handoff":
|
|
8872
|
+
return `${baseUri}/handoffs/${uriSegment(metadata.status)}/${projectSegment}`;
|
|
8873
|
+
case "incident":
|
|
8874
|
+
return `${baseUri}/incidents/${uriSegment(metadata.status)}/${projectSegment}`;
|
|
8875
|
+
case "smoke":
|
|
8876
|
+
return `${baseUri}/smoke/${uriSegment(metadata.status)}`;
|
|
8877
|
+
case "durable":
|
|
8878
|
+
return metadata.status === "active" ? `${baseUri}/durable/projects/${projectSegment}` : `${baseUri}/durable/${uriSegment(metadata.status)}/${projectSegment}`;
|
|
8845
8879
|
}
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8880
|
+
}
|
|
8881
|
+
function shouldUseStableMemoryUri(metadata) {
|
|
8882
|
+
return metadata.status === "active" && metadata.topic !== void 0 && metadata.kind !== "smoke";
|
|
8883
|
+
}
|
|
8884
|
+
async function memoryWriteMode(ov, config, memoryUri, metadata) {
|
|
8885
|
+
if (!shouldUseStableMemoryUri(metadata)) {
|
|
8886
|
+
return "create";
|
|
8853
8887
|
}
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8888
|
+
return await vikingResourceExists2(ov, config, memoryUri) ? "replace" : "create";
|
|
8889
|
+
}
|
|
8890
|
+
function vikingDirectoryChain(directoryUri) {
|
|
8891
|
+
const prefix = "viking://";
|
|
8892
|
+
if (!directoryUri.startsWith(prefix)) {
|
|
8893
|
+
return [directoryUri];
|
|
8859
8894
|
}
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
const
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8895
|
+
const parts = directoryUri.slice(prefix.length).split("/").filter(Boolean);
|
|
8896
|
+
const startIndex = parts[0] === "user" && parts.length > 2 ? 3 : 1;
|
|
8897
|
+
const chain = [];
|
|
8898
|
+
for (let index = startIndex; index <= parts.length; index += 1) {
|
|
8899
|
+
chain.push(`${prefix}${parts.slice(0, index).join("/")}`);
|
|
8900
|
+
}
|
|
8901
|
+
return chain;
|
|
8902
|
+
}
|
|
8903
|
+
function formatMemoryDocument(title, metadata, body) {
|
|
8904
|
+
const header = [
|
|
8905
|
+
title,
|
|
8906
|
+
`kind: ${metadata.kind}`,
|
|
8907
|
+
`status: ${metadata.status}`,
|
|
8908
|
+
metadata.project ? `project: ${metadata.project}` : void 0,
|
|
8909
|
+
metadata.topic ? `topic: ${metadata.topic}` : void 0,
|
|
8910
|
+
`source_agent_client: ${metadata.sourceAgentClient}`,
|
|
8911
|
+
`timestamp: ${metadata.timestamp}`,
|
|
8912
|
+
metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
|
|
8913
|
+
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
|
|
8914
|
+
].filter((line) => line !== void 0);
|
|
8915
|
+
return [...header, "", body.trim()].join("\n");
|
|
8916
|
+
}
|
|
8917
|
+
function inferMemoryMetadata(memory) {
|
|
8918
|
+
const header = memory.slice(0, Math.max(0, memory.indexOf("\n\n")) || memory.length);
|
|
8919
|
+
const firstLine2 = header.split("\n")[0]?.trim();
|
|
8920
|
+
const kind = parseOptionalMemoryKind(parseHeaderValue(header, "kind")) ?? (firstLine2 === "HANDOFF" ? "handoff" : void 0);
|
|
8921
|
+
const status = parseOptionalMemoryStatus(parseHeaderValue(header, "status"));
|
|
8922
|
+
const project = normalizeOptionalMetadata(parseHeaderValue(header, "project")) ?? normalizeOptionalMetadata(parseHeaderValue(header, "repo"));
|
|
8923
|
+
const topic = normalizeOptionalMetadata(parseHeaderValue(header, "topic")) ?? normalizeOptionalMetadata(parseHeaderValue(header, "task"));
|
|
8924
|
+
return {
|
|
8925
|
+
kind,
|
|
8926
|
+
project,
|
|
8927
|
+
sourceAgentClient: parseHeaderValue(header, "source_agent_client") ?? void 0,
|
|
8928
|
+
status,
|
|
8929
|
+
topic
|
|
8875
8930
|
};
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8931
|
+
}
|
|
8932
|
+
function parseHeaderValue(header, key) {
|
|
8933
|
+
const prefix = `${key}:`;
|
|
8934
|
+
return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
|
|
8935
|
+
}
|
|
8936
|
+
function isClearLegacyHandoffMemory(memory) {
|
|
8937
|
+
if (/^kind:\s*/m.test(memory) || /^status:\s*/m.test(memory)) {
|
|
8938
|
+
return false;
|
|
8882
8939
|
}
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8940
|
+
const trimmed = memory.trim();
|
|
8941
|
+
if (trimmed.startsWith("HANDOFF\n")) {
|
|
8942
|
+
return true;
|
|
8943
|
+
}
|
|
8944
|
+
if (!trimmed.startsWith("MEMORY\n")) {
|
|
8945
|
+
return false;
|
|
8946
|
+
}
|
|
8947
|
+
return /^(?:#+\s*)?(?:final\s+)?handoff(?:\s+update)?\b/i.test(memoryBody(trimmed));
|
|
8948
|
+
}
|
|
8949
|
+
function memoryBody(memory) {
|
|
8950
|
+
const separatorIndex = memory.indexOf("\n\n");
|
|
8951
|
+
return separatorIndex === -1 ? "" : memory.slice(separatorIndex + 2).trim();
|
|
8952
|
+
}
|
|
8953
|
+
function inferLegacyProject(memory) {
|
|
8954
|
+
const explicit = parseHeaderValue(memory, "project") ?? parseHeaderValue(memory, "repo") ?? parseHeaderValue(memory, "repo_path") ?? /\brepo(?:_path)?\s+([~/A-Za-z0-9_.:/-]+)/.exec(memory)?.[1];
|
|
8955
|
+
if (!explicit) {
|
|
8956
|
+
return "general";
|
|
8957
|
+
}
|
|
8958
|
+
const trimmed = explicit.trim().replace(/[`.,;]+$/g, "");
|
|
8959
|
+
return trimmed.includes("/") ? (0, import_node_path5.basename)(trimmed) : trimmed;
|
|
8960
|
+
}
|
|
8961
|
+
function parseOptionalMemoryKind(value) {
|
|
8962
|
+
if (!value) {
|
|
8963
|
+
return void 0;
|
|
8964
|
+
}
|
|
8965
|
+
try {
|
|
8966
|
+
return parseMemoryKind(value);
|
|
8967
|
+
} catch (_err) {
|
|
8968
|
+
return void 0;
|
|
8969
|
+
}
|
|
8970
|
+
}
|
|
8971
|
+
function parseOptionalMemoryStatus(value) {
|
|
8972
|
+
if (!value) {
|
|
8973
|
+
return void 0;
|
|
8974
|
+
}
|
|
8975
|
+
try {
|
|
8976
|
+
return parseMemoryStatus(value);
|
|
8977
|
+
} catch (_err) {
|
|
8978
|
+
return void 0;
|
|
8979
|
+
}
|
|
8980
|
+
}
|
|
8981
|
+
function normalizeOptionalMetadata(value) {
|
|
8982
|
+
const trimmed = value?.trim();
|
|
8983
|
+
return trimmed ? trimmed : void 0;
|
|
8984
|
+
}
|
|
8985
|
+
async function legacySourceAccounts(config, options) {
|
|
8986
|
+
const explicitAccounts = options.sourceAccount?.filter((account) => account.trim().length > 0) ?? [];
|
|
8987
|
+
if (explicitAccounts.length > 0) {
|
|
8988
|
+
return uniqueStrings(explicitAccounts);
|
|
8989
|
+
}
|
|
8990
|
+
if (options.allAccounts === true) {
|
|
8991
|
+
const accounts = await childDirectoryNames(localVikingDataRoot(config));
|
|
8992
|
+
return accounts.filter((account) => !account.startsWith("_"));
|
|
8993
|
+
}
|
|
8994
|
+
return [config.account];
|
|
8995
|
+
}
|
|
8996
|
+
async function legacyMemoryCandidates(config, sourceAccounts) {
|
|
8997
|
+
const candidates = [];
|
|
8998
|
+
for (const sourceAccount of sourceAccounts) {
|
|
8999
|
+
const sessionRoot = (0, import_node_path5.join)(localVikingDataRoot(config), sourceAccount, "session");
|
|
9000
|
+
for (const sourceSession of await childDirectoryNames(sessionRoot)) {
|
|
9001
|
+
const historyRoot = (0, import_node_path5.join)(sessionRoot, sourceSession, "history");
|
|
9002
|
+
for (const sourceArchive of await childDirectoryNames(historyRoot)) {
|
|
9003
|
+
if (!sourceArchive.startsWith("archive_")) {
|
|
9004
|
+
continue;
|
|
9005
|
+
}
|
|
9006
|
+
const sourcePath = (0, import_node_path5.join)(historyRoot, sourceArchive, "messages.jsonl");
|
|
9007
|
+
for (const text of await legacyMemoryTexts(sourcePath)) {
|
|
9008
|
+
candidates.push({
|
|
9009
|
+
comparableHash: sha256(comparableMemoryText(text)),
|
|
9010
|
+
hash: sha256(text),
|
|
9011
|
+
sourceAccount,
|
|
9012
|
+
sourceArchive,
|
|
9013
|
+
sourceSession,
|
|
9014
|
+
text
|
|
9015
|
+
});
|
|
9016
|
+
}
|
|
9017
|
+
}
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
9020
|
+
return candidates.sort((left, right) => legacySourceLabel(left).localeCompare(legacySourceLabel(right)));
|
|
9021
|
+
}
|
|
9022
|
+
async function legacyMemoryTexts(sourcePath) {
|
|
9023
|
+
const raw = await readTextIfExists(sourcePath);
|
|
9024
|
+
if (!raw) {
|
|
9025
|
+
return [];
|
|
9026
|
+
}
|
|
9027
|
+
const memories = [];
|
|
9028
|
+
for (const line of raw.split("\n")) {
|
|
9029
|
+
const trimmedLine = line.trim();
|
|
9030
|
+
if (!trimmedLine) {
|
|
9031
|
+
continue;
|
|
9032
|
+
}
|
|
9033
|
+
try {
|
|
9034
|
+
const parsed = JSON.parse(trimmedLine);
|
|
9035
|
+
const text = legacyMessageText(parsed)?.trim();
|
|
9036
|
+
if (text && isLegacyThreadnoteMemory(text)) {
|
|
9037
|
+
memories.push(text);
|
|
9038
|
+
}
|
|
9039
|
+
} catch (_err) {
|
|
9040
|
+
continue;
|
|
9041
|
+
}
|
|
8887
9042
|
}
|
|
9043
|
+
return memories;
|
|
8888
9044
|
}
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
const gitignorePath = (0, import_node_path9.join)(worktree, ".gitignore");
|
|
8893
|
-
const existing = await readFileIfExists(gitignorePath) ?? "";
|
|
8894
|
-
const lines = existing.split("\n").map((line) => line.trim());
|
|
8895
|
-
const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
|
|
8896
|
-
if (missingPatterns.length === 0) {
|
|
8897
|
-
return;
|
|
9045
|
+
function legacyMessageText(value) {
|
|
9046
|
+
if (!isJsonObject(value)) {
|
|
9047
|
+
return void 0;
|
|
8898
9048
|
}
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
if (existing.length > 0 && !existing.endsWith("\n")) {
|
|
8902
|
-
segments.push("\n");
|
|
9049
|
+
if (typeof value.content === "string") {
|
|
9050
|
+
return value.content;
|
|
8903
9051
|
}
|
|
8904
|
-
if (
|
|
8905
|
-
|
|
9052
|
+
if (!Array.isArray(value.parts)) {
|
|
9053
|
+
return void 0;
|
|
8906
9054
|
}
|
|
8907
|
-
|
|
8908
|
-
|
|
9055
|
+
const parts = value.parts.map((part) => isJsonObject(part) && part.type === "text" && typeof part.text === "string" ? part.text : void 0).filter((text) => text !== void 0);
|
|
9056
|
+
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
9057
|
+
}
|
|
9058
|
+
function isLegacyThreadnoteMemory(text) {
|
|
9059
|
+
return text.startsWith("MEMORY\n") || text.startsWith("HANDOFF\n");
|
|
9060
|
+
}
|
|
9061
|
+
async function existingDurableMemoryHashes(config) {
|
|
9062
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
9063
|
+
await collectDurableMemoryHashes(localVikingDataRoot(config), hashes);
|
|
9064
|
+
return hashes;
|
|
9065
|
+
}
|
|
9066
|
+
async function collectDurableMemoryHashes(root, hashes) {
|
|
9067
|
+
let entries;
|
|
9068
|
+
try {
|
|
9069
|
+
entries = await (0, import_promises5.readdir)(root, { withFileTypes: true });
|
|
9070
|
+
} catch (_err) {
|
|
9071
|
+
return;
|
|
8909
9072
|
}
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
);
|
|
8925
|
-
return;
|
|
9073
|
+
for (const entry of entries) {
|
|
9074
|
+
const path = (0, import_node_path5.join)(root, entry.name);
|
|
9075
|
+
if (entry.isDirectory()) {
|
|
9076
|
+
await collectDurableMemoryHashes(path, hashes);
|
|
9077
|
+
continue;
|
|
9078
|
+
}
|
|
9079
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md") || !isDurableMemoryPath(path)) {
|
|
9080
|
+
continue;
|
|
9081
|
+
}
|
|
9082
|
+
const content = await readTextIfExists(path);
|
|
9083
|
+
if (content) {
|
|
9084
|
+
const trimmedContent = content.trim();
|
|
9085
|
+
hashes.add(sha256(trimmedContent));
|
|
9086
|
+
hashes.add(sha256(comparableMemoryText(trimmedContent)));
|
|
8926
9087
|
}
|
|
8927
|
-
}
|
|
8928
|
-
if (push) {
|
|
8929
|
-
await maybeRun(false, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
8930
9088
|
}
|
|
8931
9089
|
}
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
allowFailure: true
|
|
8942
|
-
});
|
|
8943
|
-
const ahead = await gitOutput(team.config.worktree, ["rev-list", "--count", "@{u}..HEAD"], options.dryRun === true);
|
|
8944
|
-
const behind = await gitOutput(team.config.worktree, ["rev-list", "--count", "HEAD..@{u}"], options.dryRun === true);
|
|
8945
|
-
if (ahead !== void 0) {
|
|
8946
|
-
console.log(`Ahead of upstream: ${ahead}`);
|
|
9090
|
+
function isDurableMemoryPath(path) {
|
|
9091
|
+
return path.split(import_node_path5.sep).includes("memories");
|
|
9092
|
+
}
|
|
9093
|
+
async function childDirectoryNames(path) {
|
|
9094
|
+
let entries;
|
|
9095
|
+
try {
|
|
9096
|
+
entries = await (0, import_promises5.readdir)(path, { withFileTypes: true });
|
|
9097
|
+
} catch (_err) {
|
|
9098
|
+
return [];
|
|
8947
9099
|
}
|
|
8948
|
-
|
|
8949
|
-
|
|
9100
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
9101
|
+
}
|
|
9102
|
+
async function readTextIfExists(path) {
|
|
9103
|
+
try {
|
|
9104
|
+
const pathStat = await (0, import_promises5.stat)(path);
|
|
9105
|
+
if (!pathStat.isFile()) {
|
|
9106
|
+
return void 0;
|
|
9107
|
+
}
|
|
9108
|
+
return await (0, import_promises5.readFile)(path, "utf8");
|
|
9109
|
+
} catch (_err) {
|
|
9110
|
+
return void 0;
|
|
8950
9111
|
}
|
|
8951
9112
|
}
|
|
8952
|
-
|
|
8953
|
-
const
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
9113
|
+
function sensitiveMemoryReason(text) {
|
|
9114
|
+
const patterns = [
|
|
9115
|
+
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
9116
|
+
{ name: "API key", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
9117
|
+
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
9118
|
+
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
9119
|
+
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ }
|
|
9120
|
+
];
|
|
9121
|
+
return patterns.find((pattern) => pattern.regex.test(text))?.name;
|
|
9122
|
+
}
|
|
9123
|
+
function comparableMemoryText(text) {
|
|
9124
|
+
const trimmed = text.trim();
|
|
9125
|
+
if (!trimmed.startsWith("MEMORY\n")) {
|
|
9126
|
+
return trimmed;
|
|
8959
9127
|
}
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
9128
|
+
const separatorIndex = trimmed.indexOf("\n\n");
|
|
9129
|
+
return separatorIndex === -1 ? trimmed : trimmed.slice(separatorIndex + 2).trim();
|
|
9130
|
+
}
|
|
9131
|
+
function legacySourceLabel(candidate) {
|
|
9132
|
+
return `${candidate.sourceAccount}/${candidate.sourceSession}/${candidate.sourceArchive}`;
|
|
9133
|
+
}
|
|
9134
|
+
function localVikingDataRoot(config) {
|
|
9135
|
+
return (0, import_node_path5.join)(config.agentContextHome, "data", "viking");
|
|
9136
|
+
}
|
|
9137
|
+
function localUserMemoriesRoot(config) {
|
|
9138
|
+
return (0, import_node_path5.join)(localVikingDataRoot(config), config.account, "user", uriSegment(config.user), "memories");
|
|
9139
|
+
}
|
|
9140
|
+
function uniqueStrings(values) {
|
|
9141
|
+
return [...new Set(values)].sort();
|
|
9142
|
+
}
|
|
9143
|
+
function isResourceBusy(stderr, stdout) {
|
|
9144
|
+
const output2 = `${stderr}
|
|
9145
|
+
${stdout}`.toLowerCase();
|
|
9146
|
+
return output2.includes("resource is busy") || output2.includes("resource is being processed");
|
|
9147
|
+
}
|
|
9148
|
+
async function buildHandoff(options) {
|
|
9149
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]) ?? getInvocationCwd();
|
|
9150
|
+
const branch = await gitValue(["branch", "--show-current"], repoRoot) ?? "unknown";
|
|
9151
|
+
const status = await gitValue(["status", "--short"], repoRoot) ?? "";
|
|
9152
|
+
const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
|
|
9153
|
+
const touchedFiles = await gitTouchedFiles(repoRoot);
|
|
9154
|
+
const repoName = (0, import_node_path5.basename)(repoRoot);
|
|
9155
|
+
const metadata = {
|
|
9156
|
+
kind: "handoff",
|
|
9157
|
+
project: normalizeOptionalMetadata(options.project) ?? repoName,
|
|
9158
|
+
sourceAgentClient: options.sourceAgentClient ?? "codex",
|
|
9159
|
+
status: "active",
|
|
9160
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9161
|
+
topic: normalizeOptionalMetadata(options.topic)
|
|
9162
|
+
};
|
|
9163
|
+
const bodyText = [
|
|
9164
|
+
`repo: ${repoName}`,
|
|
9165
|
+
`repo_path: ${repoRoot}`,
|
|
9166
|
+
`branch: ${branch || "unknown"}`,
|
|
9167
|
+
`task: ${options.task ?? "unspecified"}`,
|
|
9168
|
+
"",
|
|
9169
|
+
"files_touched:",
|
|
9170
|
+
formatBlock(touchedFiles, "- none"),
|
|
9171
|
+
"",
|
|
9172
|
+
"git_status:",
|
|
9173
|
+
formatBlock(status, "- clean"),
|
|
9174
|
+
"",
|
|
9175
|
+
"diff_stat:",
|
|
9176
|
+
formatBlock(diffStat, "- none"),
|
|
9177
|
+
"",
|
|
9178
|
+
"tests:",
|
|
9179
|
+
options.tests ?? "- not recorded",
|
|
9180
|
+
"",
|
|
9181
|
+
"blockers:",
|
|
9182
|
+
options.blockers ?? "- none recorded",
|
|
9183
|
+
"",
|
|
9184
|
+
"next_step:",
|
|
9185
|
+
options.nextStep ?? "- inspect the current repo state and continue from this handoff"
|
|
9186
|
+
].join("\n");
|
|
9187
|
+
return { bodyText, metadata };
|
|
9188
|
+
}
|
|
9189
|
+
async function gitTouchedFiles(cwd) {
|
|
9190
|
+
const changedFiles = await gitValue(["diff", "--name-only", "HEAD"], cwd);
|
|
9191
|
+
const untrackedFiles = await gitValue(["ls-files", "--others", "--exclude-standard"], cwd);
|
|
9192
|
+
const files = /* @__PURE__ */ new Set();
|
|
9193
|
+
for (const value of [changedFiles, untrackedFiles]) {
|
|
9194
|
+
for (const line of (value ?? "").split("\n")) {
|
|
9195
|
+
const trimmed = line.trim();
|
|
9196
|
+
if (trimmed) {
|
|
9197
|
+
files.add(trimmed);
|
|
9198
|
+
}
|
|
8965
9199
|
}
|
|
8966
|
-
const message = options.message ?? `share: sync ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
8967
|
-
await stageShareableChanges(dryRun, git, worktree);
|
|
8968
|
-
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
8969
9200
|
}
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
if (await exists((0, import_node_path9.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path9.join)(team.config.gitdir, "rebase-apply"))) {
|
|
8977
|
-
throw new Error(
|
|
8978
|
-
`git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
|
|
8979
|
-
Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
|
|
8980
|
-
);
|
|
8981
|
-
}
|
|
8982
|
-
throw new Error(
|
|
8983
|
-
`git pull --rebase failed in ${worktree}: ${pullResult.stderr.trim() || pullResult.stdout.trim() || "unknown error"}`
|
|
8984
|
-
);
|
|
9201
|
+
return [...files].sort().join("\n");
|
|
9202
|
+
}
|
|
9203
|
+
function formatBlock(value, emptyValue) {
|
|
9204
|
+
const trimmed = value.trim();
|
|
9205
|
+
if (!trimmed) {
|
|
9206
|
+
return emptyValue;
|
|
8985
9207
|
}
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
9208
|
+
return trimmed.split("\n").map((line) => `- ${line}`).join("\n");
|
|
9209
|
+
}
|
|
9210
|
+
|
|
9211
|
+
// src/update-check.ts
|
|
9212
|
+
var import_node_child_process2 = require("node:child_process");
|
|
9213
|
+
var import_promises6 = require("node:fs/promises");
|
|
9214
|
+
var import_node_path6 = require("node:path");
|
|
9215
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
9216
|
+
var NPM_LATEST_URL = "https://registry.npmjs.org/threadnote/latest";
|
|
9217
|
+
var FETCH_TIMEOUT_MS = 3e3;
|
|
9218
|
+
async function checkForThreadnoteUpdate(args) {
|
|
9219
|
+
if (args.currentVersion === "unknown") {
|
|
9220
|
+
return void 0;
|
|
9221
|
+
}
|
|
9222
|
+
const cached = await readUpdateCache(args.cachePath);
|
|
9223
|
+
if (cached && isCacheFresh(cached)) {
|
|
9224
|
+
return compareVersions(args.currentVersion, cached.latestVersion);
|
|
8993
9225
|
}
|
|
8994
|
-
|
|
8995
|
-
|
|
9226
|
+
const fresh = await fetchLatestVersion();
|
|
9227
|
+
if (fresh) {
|
|
9228
|
+
await writeUpdateCache(args.cachePath, {
|
|
9229
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9230
|
+
latestVersion: fresh,
|
|
9231
|
+
version: 1
|
|
9232
|
+
});
|
|
9233
|
+
return compareVersions(args.currentVersion, fresh);
|
|
8996
9234
|
}
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
const pathspecs = [":(top)README.md", ":(top).gitignore", ...SHAREABLE_MEMORY_KIND_DIRS.map((dir) => `:(top)${dir}`)];
|
|
9000
|
-
await maybeRun(dryRun, git, ["-C", worktree, "add", "--", ...pathspecs], { allowFailure: true });
|
|
9001
|
-
}
|
|
9002
|
-
async function runSharePublish(config, sourceUri, options) {
|
|
9003
|
-
assertVikingUri(sourceUri);
|
|
9004
|
-
const team = await resolveTeam(config, options.team);
|
|
9005
|
-
const dryRun = options.dryRun === true;
|
|
9006
|
-
const preview = options.preview === true;
|
|
9007
|
-
if (isInSharedNamespace(config, sourceUri)) {
|
|
9008
|
-
throw new Error(`Memory ${sourceUri} is already in the shared namespace.`);
|
|
9235
|
+
if (cached) {
|
|
9236
|
+
return compareVersions(args.currentVersion, cached.latestVersion);
|
|
9009
9237
|
}
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
console.log(`PREVIEW source: ${sourceUri}`);
|
|
9017
|
-
console.log(`PREVIEW destination: ${targetUri}`);
|
|
9018
|
-
if (scrub.blocker) {
|
|
9019
|
-
console.log(
|
|
9020
|
-
`PREVIEW BLOCKED: ${scrub.blocker}. Strip the sensitive value or rerun with --redact for soft-leak patterns.`
|
|
9021
|
-
);
|
|
9238
|
+
return void 0;
|
|
9239
|
+
}
|
|
9240
|
+
function spawnDetachedAutoUpdate() {
|
|
9241
|
+
try {
|
|
9242
|
+
const entry = process.argv[1];
|
|
9243
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
9022
9244
|
return;
|
|
9023
9245
|
}
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
return;
|
|
9246
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [entry, "update", "--yes"], {
|
|
9247
|
+
detached: true,
|
|
9248
|
+
stdio: "ignore"
|
|
9249
|
+
});
|
|
9250
|
+
child.unref();
|
|
9251
|
+
} catch {
|
|
9031
9252
|
}
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9253
|
+
}
|
|
9254
|
+
function compareVersions(currentVersion, latestVersion) {
|
|
9255
|
+
return {
|
|
9256
|
+
currentVersion,
|
|
9257
|
+
latestVersion,
|
|
9258
|
+
outdated: isNewerVersion(latestVersion, currentVersion)
|
|
9259
|
+
};
|
|
9260
|
+
}
|
|
9261
|
+
function isNewerVersion(candidate, baseline) {
|
|
9262
|
+
const candidateParts = parseVersion(candidate);
|
|
9263
|
+
const baselineParts = parseVersion(baseline);
|
|
9264
|
+
for (let index = 0; index < 3; index += 1) {
|
|
9265
|
+
if (candidateParts[index] !== baselineParts[index]) {
|
|
9266
|
+
return candidateParts[index] > baselineParts[index];
|
|
9267
|
+
}
|
|
9036
9268
|
}
|
|
9037
|
-
|
|
9038
|
-
|
|
9269
|
+
return false;
|
|
9270
|
+
}
|
|
9271
|
+
function parseVersion(value) {
|
|
9272
|
+
const parts = value.split(".").map((part) => parseInt(part, 10));
|
|
9273
|
+
return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
|
|
9274
|
+
}
|
|
9275
|
+
function isCacheFresh(cache) {
|
|
9276
|
+
const checkedAt = new Date(cache.checkedAt).getTime();
|
|
9277
|
+
return Number.isFinite(checkedAt) && Date.now() - checkedAt < CACHE_TTL_MS;
|
|
9278
|
+
}
|
|
9279
|
+
async function readUpdateCache(cachePath) {
|
|
9280
|
+
try {
|
|
9281
|
+
const raw = await (0, import_promises6.readFile)(cachePath, "utf8");
|
|
9282
|
+
const parsed = JSON.parse(raw);
|
|
9283
|
+
if (parsed.version !== 1 || typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
|
|
9284
|
+
return void 0;
|
|
9285
|
+
}
|
|
9286
|
+
return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, version: 1 };
|
|
9287
|
+
} catch {
|
|
9288
|
+
return void 0;
|
|
9039
9289
|
}
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
)
|
|
9290
|
+
}
|
|
9291
|
+
async function writeUpdateCache(cachePath, contents) {
|
|
9292
|
+
try {
|
|
9293
|
+
await (0, import_promises6.mkdir)((0, import_node_path6.dirname)(cachePath), { recursive: true });
|
|
9294
|
+
await (0, import_promises6.writeFile)(cachePath, `${JSON.stringify(contents)}
|
|
9295
|
+
`, { encoding: "utf8", mode: 384 });
|
|
9296
|
+
} catch {
|
|
9045
9297
|
}
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
if (options.push !== false) {
|
|
9056
|
-
const pushResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
9057
|
-
if (dryRun) {
|
|
9058
|
-
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
9059
|
-
} else if (pushResult && pushResult.exitCode !== 0) {
|
|
9060
|
-
const detail = pushResult.stderr.trim() || pushResult.stdout.trim() || "unknown error";
|
|
9061
|
-
throw new Error(
|
|
9062
|
-
`Memory was committed locally but git push failed: ${detail}
|
|
9063
|
-
Resolve the remote issue (auth, network, branch protection), then run: threadnote share sync`
|
|
9064
|
-
);
|
|
9298
|
+
}
|
|
9299
|
+
async function fetchLatestVersion() {
|
|
9300
|
+
try {
|
|
9301
|
+
const response = await fetch(NPM_LATEST_URL, {
|
|
9302
|
+
headers: { accept: "application/json" },
|
|
9303
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
9304
|
+
});
|
|
9305
|
+
if (!response.ok) {
|
|
9306
|
+
return void 0;
|
|
9065
9307
|
}
|
|
9308
|
+
const data = await response.json();
|
|
9309
|
+
return typeof data.version === "string" && data.version.length > 0 ? data.version : void 0;
|
|
9310
|
+
} catch {
|
|
9311
|
+
return void 0;
|
|
9066
9312
|
}
|
|
9067
|
-
console.log(`Published ${sourceUri} -> ${targetUri}`);
|
|
9068
9313
|
}
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
const content = await readMemoryContent(config, ov, sourceUri, dryRun);
|
|
9078
|
-
const targetUri = personalUriFor(config, sourceUri, team.name);
|
|
9079
|
-
if (!dryRun && await vikingResourceExists2(ov, config, targetUri)) {
|
|
9080
|
-
throw new Error(
|
|
9081
|
-
`Refusing to unpublish: a personal memory already exists at ${targetUri}. Move or forget it first, then retry.`
|
|
9082
|
-
);
|
|
9314
|
+
|
|
9315
|
+
// src/version.ts
|
|
9316
|
+
var import_node_fs4 = require("node:fs");
|
|
9317
|
+
var import_node_path7 = require("node:path");
|
|
9318
|
+
var cachedVersion;
|
|
9319
|
+
function getThreadnoteVersion() {
|
|
9320
|
+
if (cachedVersion !== void 0) {
|
|
9321
|
+
return cachedVersion;
|
|
9083
9322
|
}
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
await maybeRun(dryRun, git, ["-C", worktree, "rm", relativePath], { allowFailure: true });
|
|
9091
|
-
await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
|
|
9092
|
-
if (options.push !== false) {
|
|
9093
|
-
await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
9323
|
+
try {
|
|
9324
|
+
const packageJsonPath = (0, import_node_path7.join)(__dirname, "..", "package.json");
|
|
9325
|
+
const parsed = JSON.parse((0, import_node_fs4.readFileSync)(packageJsonPath, "utf8"));
|
|
9326
|
+
cachedVersion = typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : "unknown";
|
|
9327
|
+
} catch {
|
|
9328
|
+
cachedVersion = "unknown";
|
|
9094
9329
|
}
|
|
9095
|
-
|
|
9330
|
+
return cachedVersion;
|
|
9096
9331
|
}
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9332
|
+
|
|
9333
|
+
// src/hooks.ts
|
|
9334
|
+
var MANAGED_HOOKS = [
|
|
9335
|
+
{
|
|
9336
|
+
event: "PreCompact",
|
|
9337
|
+
command: HOOK_PRE_COMPACT_COMMAND,
|
|
9338
|
+
description: "Auto-store a handoff snapshot before context compaction so the next turn can recall it."
|
|
9339
|
+
},
|
|
9340
|
+
{
|
|
9341
|
+
event: "SessionStart",
|
|
9342
|
+
command: HOOK_SESSION_START_COMMAND,
|
|
9343
|
+
description: "Pre-load the latest handoff for the current repo into the new session context."
|
|
9103
9344
|
}
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9345
|
+
];
|
|
9346
|
+
async function runHooksInstall(config, agent, options) {
|
|
9347
|
+
const apply = options.apply === true && options.dryRun !== true;
|
|
9348
|
+
const remove = options.remove === true;
|
|
9349
|
+
switch (agent) {
|
|
9350
|
+
case "claude":
|
|
9351
|
+
await runClaudeHooksInstall({ apply, remove });
|
|
9352
|
+
return;
|
|
9353
|
+
case "codex":
|
|
9354
|
+
printCodexHooksNotice(remove);
|
|
9355
|
+
return;
|
|
9356
|
+
case "cursor":
|
|
9357
|
+
printNoHooksSupported("cursor", remove);
|
|
9358
|
+
return;
|
|
9359
|
+
case "copilot":
|
|
9360
|
+
printNoHooksSupported("copilot", remove);
|
|
9361
|
+
return;
|
|
9111
9362
|
}
|
|
9112
9363
|
}
|
|
9113
|
-
async function
|
|
9114
|
-
const
|
|
9115
|
-
const
|
|
9116
|
-
const
|
|
9117
|
-
const
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
}
|
|
9364
|
+
async function runClaudeHooksInstall(options) {
|
|
9365
|
+
const path = expandPath(CLAUDE_SETTINGS_PATH);
|
|
9366
|
+
const existingRaw = await exists(path) ? await (0, import_promises7.readFile)(path, "utf8") : "{}";
|
|
9367
|
+
const parsed = parseJsonConfigObject(existingRaw) ?? {};
|
|
9368
|
+
const next = options.remove ? withoutThreadnoteHooks(parsed) : withThreadnoteHooks(parsed);
|
|
9369
|
+
const before = JSON.stringify(parsed);
|
|
9370
|
+
const after = JSON.stringify(next);
|
|
9371
|
+
if (before === after) {
|
|
9372
|
+
console.log(`Claude hooks already ${options.remove ? "absent" : "managed"} in ${path}.`);
|
|
9373
|
+
return;
|
|
9122
9374
|
}
|
|
9123
|
-
|
|
9124
|
-
const
|
|
9125
|
-
|
|
9126
|
-
if (dryRun) {
|
|
9127
|
-
console.log(`Would update teams file: ${teamsFilePath(config)}`);
|
|
9128
|
-
} else {
|
|
9129
|
-
await writeTeamsFile(config, updated);
|
|
9130
|
-
console.log(`Removed team "${team.name}" from teams.json.`);
|
|
9375
|
+
console.log(`${options.apply ? "Updating" : "Would update"} ${path}:`);
|
|
9376
|
+
for (const entry of MANAGED_HOOKS) {
|
|
9377
|
+
console.log(` ${options.remove ? "-" : "+"} ${entry.event}: ${entry.command}`);
|
|
9131
9378
|
}
|
|
9132
|
-
if (options.
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
}
|
|
9136
|
-
|
|
9379
|
+
if (!options.apply) {
|
|
9380
|
+
console.log("\nRe-run with --apply to actually modify the file.");
|
|
9381
|
+
return;
|
|
9382
|
+
}
|
|
9383
|
+
await (0, import_promises7.mkdir)((0, import_node_path8.dirname)(path), { recursive: true });
|
|
9384
|
+
const serialized = `${JSON.stringify(next, void 0, 2)}
|
|
9385
|
+
`;
|
|
9386
|
+
await (0, import_promises7.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
|
|
9387
|
+
await (0, import_promises7.chmod)(path, 384);
|
|
9388
|
+
console.log(`${options.remove ? "Removed" : "Installed"} threadnote-managed Claude hooks.`);
|
|
9389
|
+
}
|
|
9390
|
+
function withThreadnoteHooks(input2) {
|
|
9391
|
+
const hooks = ensureMutableObject(input2.hooks);
|
|
9392
|
+
for (const entry of MANAGED_HOOKS) {
|
|
9393
|
+
const list = ensureMutableArray(hooks[entry.event]).filter((item) => !isManagedThreadnoteEntry(item));
|
|
9394
|
+
list.push({
|
|
9395
|
+
[THREADNOTE_HOOK_MARKER]: THREADNOTE_HOOK_MARKER_VALUE,
|
|
9396
|
+
matcher: "",
|
|
9397
|
+
hooks: [{ type: "command", command: entry.command }]
|
|
9398
|
+
});
|
|
9399
|
+
hooks[entry.event] = list;
|
|
9137
9400
|
}
|
|
9401
|
+
return { ...input2, hooks };
|
|
9138
9402
|
}
|
|
9139
|
-
function
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
return "default";
|
|
9403
|
+
function withoutThreadnoteHooks(input2) {
|
|
9404
|
+
if (!isJsonObject(input2.hooks)) {
|
|
9405
|
+
return input2;
|
|
9143
9406
|
}
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9407
|
+
const hooks = {};
|
|
9408
|
+
for (const [event, value] of Object.entries(input2.hooks)) {
|
|
9409
|
+
if (!Array.isArray(value)) {
|
|
9410
|
+
hooks[event] = value;
|
|
9411
|
+
continue;
|
|
9412
|
+
}
|
|
9413
|
+
const filtered = value.filter((item) => !isManagedThreadnoteEntry(item));
|
|
9414
|
+
if (filtered.length > 0) {
|
|
9415
|
+
hooks[event] = filtered;
|
|
9416
|
+
}
|
|
9148
9417
|
}
|
|
9149
|
-
|
|
9418
|
+
if (Object.keys(hooks).length === 0) {
|
|
9419
|
+
const next = { ...input2 };
|
|
9420
|
+
delete next.hooks;
|
|
9421
|
+
return next;
|
|
9422
|
+
}
|
|
9423
|
+
return { ...input2, hooks };
|
|
9150
9424
|
}
|
|
9151
|
-
function
|
|
9152
|
-
return (
|
|
9425
|
+
function isManagedThreadnoteEntry(value) {
|
|
9426
|
+
return isJsonObject(value) && value[THREADNOTE_HOOK_MARKER] === THREADNOTE_HOOK_MARKER_VALUE;
|
|
9153
9427
|
}
|
|
9154
|
-
function
|
|
9155
|
-
return (
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
"
|
|
9163
|
-
|
|
9164
|
-
|
|
9428
|
+
function ensureMutableObject(value) {
|
|
9429
|
+
return isJsonObject(value) ? { ...value } : {};
|
|
9430
|
+
}
|
|
9431
|
+
function ensureMutableArray(value) {
|
|
9432
|
+
return Array.isArray(value) ? [...value] : [];
|
|
9433
|
+
}
|
|
9434
|
+
function printCodexHooksNotice(remove) {
|
|
9435
|
+
if (remove) {
|
|
9436
|
+
console.log("Codex CLI does not expose a managed hook surface today, so there is nothing to remove.");
|
|
9437
|
+
return;
|
|
9438
|
+
}
|
|
9439
|
+
console.log(
|
|
9440
|
+
[
|
|
9441
|
+
"Codex CLI does not currently expose lifecycle hooks (no PreCompact or SessionStart analog).",
|
|
9442
|
+
"Threadnote already installs Codex user instructions at ~/.codex/AGENTS.md; that remains the active guidance surface.",
|
|
9443
|
+
"If a future Codex release adds hook events, threadnote will pick them up via `install-hooks codex`."
|
|
9444
|
+
].join("\n")
|
|
9165
9445
|
);
|
|
9166
9446
|
}
|
|
9167
|
-
function
|
|
9168
|
-
|
|
9447
|
+
function printNoHooksSupported(agent, remove) {
|
|
9448
|
+
if (remove) {
|
|
9449
|
+
console.log(`${agent} does not expose a hook surface; nothing to remove.`);
|
|
9450
|
+
return;
|
|
9451
|
+
}
|
|
9452
|
+
console.log(
|
|
9453
|
+
[
|
|
9454
|
+
`${agent} does not expose agent-mode hooks today.`,
|
|
9455
|
+
"Threadnote already installs user-level instructions for this agent; that remains the active guidance surface.",
|
|
9456
|
+
"Hooks support will be added if the agent gains a hook surface upstream."
|
|
9457
|
+
].join("\n")
|
|
9458
|
+
);
|
|
9169
9459
|
}
|
|
9170
|
-
async function
|
|
9171
|
-
const path =
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
return { teams: {}, version: TEAMS_FILE_VERSION };
|
|
9460
|
+
async function hasManagedClaudeHooks() {
|
|
9461
|
+
const path = expandPath(CLAUDE_SETTINGS_PATH);
|
|
9462
|
+
if (!await exists(path)) {
|
|
9463
|
+
return false;
|
|
9175
9464
|
}
|
|
9465
|
+
const raw = await (0, import_promises7.readFile)(path, "utf8");
|
|
9176
9466
|
const parsed = parseJsonConfigObject(raw);
|
|
9177
|
-
if (!parsed) {
|
|
9178
|
-
|
|
9179
|
-
}
|
|
9180
|
-
if (typeof parsed.version === "number" && parsed.version > TEAMS_FILE_VERSION) {
|
|
9181
|
-
throw new Error(
|
|
9182
|
-
`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.`
|
|
9183
|
-
);
|
|
9467
|
+
if (!parsed || !isJsonObject(parsed.hooks)) {
|
|
9468
|
+
return false;
|
|
9184
9469
|
}
|
|
9185
|
-
const
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
}
|
|
9192
|
-
const entry = value;
|
|
9193
|
-
if (typeof entry.remote !== "string" || entry.remote.length === 0) {
|
|
9194
|
-
console.warn(`Skipping team entry "${name}" in ${path}: missing or empty "remote" field.`);
|
|
9195
|
-
continue;
|
|
9196
|
-
}
|
|
9197
|
-
teams[name] = {
|
|
9198
|
-
addedAt: typeof entry.addedAt === "string" ? entry.addedAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
9199
|
-
gitdir: typeof entry.gitdir === "string" ? entry.gitdir : teamGitdirPath(config, name),
|
|
9200
|
-
name,
|
|
9201
|
-
remote: entry.remote,
|
|
9202
|
-
worktree: typeof entry.worktree === "string" ? entry.worktree : teamWorktreePath(config, name)
|
|
9203
|
-
};
|
|
9470
|
+
for (const value of Object.values(parsed.hooks)) {
|
|
9471
|
+
if (!Array.isArray(value)) {
|
|
9472
|
+
continue;
|
|
9473
|
+
}
|
|
9474
|
+
if (value.some((item) => isManagedThreadnoteEntry(item))) {
|
|
9475
|
+
return true;
|
|
9204
9476
|
}
|
|
9205
9477
|
}
|
|
9206
|
-
|
|
9207
|
-
return { defaultTeam, teams, version: TEAMS_FILE_VERSION };
|
|
9208
|
-
}
|
|
9209
|
-
async function writeTeamsFile(config, contents) {
|
|
9210
|
-
const path = teamsFilePath(config);
|
|
9211
|
-
await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(path), { recursive: true });
|
|
9212
|
-
const serializable = {
|
|
9213
|
-
defaultTeam: contents.defaultTeam,
|
|
9214
|
-
teams: contents.teams,
|
|
9215
|
-
version: contents.version
|
|
9216
|
-
};
|
|
9217
|
-
await (0, import_promises8.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
|
|
9218
|
-
`, { encoding: "utf8", mode: 384 });
|
|
9219
|
-
}
|
|
9220
|
-
async function resolveTeam(config, requested) {
|
|
9221
|
-
const teamsFile = await readTeamsFile(config);
|
|
9222
|
-
const entries = Object.entries(teamsFile.teams);
|
|
9223
|
-
if (entries.length === 0) {
|
|
9224
|
-
throw new Error("No shared teams configured. Run: threadnote share init <remote-url>");
|
|
9225
|
-
}
|
|
9226
|
-
const wantName = requested ? normalizeTeamName(requested) : teamsFile.defaultTeam ?? entries[0][0];
|
|
9227
|
-
const found = teamsFile.teams[wantName];
|
|
9228
|
-
if (!found) {
|
|
9229
|
-
const known = entries.map(([name]) => name).join(", ");
|
|
9230
|
-
throw new Error(`Team "${wantName}" is not configured. Known teams: ${known}`);
|
|
9231
|
-
}
|
|
9232
|
-
return { config: found, name: wantName };
|
|
9478
|
+
return false;
|
|
9233
9479
|
}
|
|
9234
|
-
function
|
|
9235
|
-
|
|
9236
|
-
|
|
9480
|
+
async function runPreCompactHook(config, options = {}) {
|
|
9481
|
+
try {
|
|
9482
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
9483
|
+
const project = repoRoot ? (0, import_node_path8.basename)(repoRoot) : "general";
|
|
9484
|
+
await runHandoff(config, {
|
|
9485
|
+
blockers: "- none recorded",
|
|
9486
|
+
dryRun: options.dryRun === true,
|
|
9487
|
+
nextStep: "Continue from this auto-snapshot. A manual `threadnote handoff` will produce a richer write-up if you have more context.",
|
|
9488
|
+
project,
|
|
9489
|
+
sourceAgentClient: "claude",
|
|
9490
|
+
task: "Auto-snapshot captured at Claude PreCompact (deterministic safety net before context compaction).",
|
|
9491
|
+
tests: "- not recorded (auto-snapshot)",
|
|
9492
|
+
topic: HOOK_AUTO_PRECOMPACT_TOPIC
|
|
9493
|
+
});
|
|
9494
|
+
} catch (err) {
|
|
9495
|
+
process.stderr.write(
|
|
9496
|
+
`threadnote pre-compact-hook: snapshot skipped (${err instanceof Error ? err.message : String(err)})
|
|
9497
|
+
`
|
|
9498
|
+
);
|
|
9237
9499
|
}
|
|
9238
|
-
return existing.defaultTeam === void 0;
|
|
9239
9500
|
}
|
|
9240
|
-
async function
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9501
|
+
async function runSessionStartHook(config, options = {}) {
|
|
9502
|
+
try {
|
|
9503
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
9504
|
+
if (!repoRoot) {
|
|
9505
|
+
return;
|
|
9506
|
+
}
|
|
9507
|
+
const project = (0, import_node_path8.basename)(repoRoot);
|
|
9508
|
+
await emitUpdateBannerIfOutdated(config);
|
|
9509
|
+
process.stdout.write(`## Threadnote \u2014 latest context for ${project}
|
|
9510
|
+
|
|
9511
|
+
`);
|
|
9512
|
+
await runRecall(config, {
|
|
9513
|
+
dryRun: options.dryRun === true,
|
|
9514
|
+
inferScope: true,
|
|
9515
|
+
nodeLimit: "5",
|
|
9516
|
+
query: `${project} latest handoff durable feature memory`
|
|
9517
|
+
});
|
|
9518
|
+
} catch (err) {
|
|
9519
|
+
process.stderr.write(
|
|
9520
|
+
`threadnote session-start-hook: recall skipped (${err instanceof Error ? err.message : String(err)})
|
|
9521
|
+
`
|
|
9253
9522
|
);
|
|
9254
9523
|
}
|
|
9255
9524
|
}
|
|
9256
|
-
async function
|
|
9257
|
-
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9525
|
+
async function emitUpdateBannerIfOutdated(config) {
|
|
9526
|
+
try {
|
|
9527
|
+
const result = await checkForThreadnoteUpdate({
|
|
9528
|
+
cachePath: (0, import_node_path8.join)(config.agentContextHome, ".update-state.json"),
|
|
9529
|
+
currentVersion: getThreadnoteVersion()
|
|
9530
|
+
});
|
|
9531
|
+
if (!result || !result.outdated) {
|
|
9532
|
+
return;
|
|
9533
|
+
}
|
|
9534
|
+
if (process.env.THREADNOTE_AUTO_UPDATE === "1") {
|
|
9535
|
+
process.stdout.write(
|
|
9536
|
+
`[threadnote] v${result.latestVersion} available (current v${result.currentVersion}). Auto-updating in the background; the new version takes effect next session.
|
|
9537
|
+
|
|
9538
|
+
`
|
|
9539
|
+
);
|
|
9540
|
+
spawnDetachedAutoUpdate();
|
|
9541
|
+
return;
|
|
9542
|
+
}
|
|
9543
|
+
process.stdout.write(
|
|
9544
|
+
`[threadnote] v${result.latestVersion} available (current v${result.currentVersion}). Run: threadnote update
|
|
9545
|
+
|
|
9546
|
+
`
|
|
9547
|
+
);
|
|
9548
|
+
} catch {
|
|
9263
9549
|
}
|
|
9264
|
-
return files.length;
|
|
9265
9550
|
}
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9551
|
+
|
|
9552
|
+
// src/seeding.ts
|
|
9553
|
+
var import_promises8 = require("node:fs/promises");
|
|
9554
|
+
var import_node_path9 = require("node:path");
|
|
9555
|
+
async function runSeed(config, options) {
|
|
9556
|
+
const manifest = await readSeedManifest(config.manifestPath);
|
|
9557
|
+
const ignorePatterns = await loadIgnorePatterns();
|
|
9558
|
+
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
9559
|
+
const statePath = (0, import_node_path9.join)(config.agentContextHome, SEED_STATE_FILE);
|
|
9560
|
+
const state = options.force === true ? { files: {}, version: 1 } : await readSeedState(statePath);
|
|
9561
|
+
const projects = filterProjects(manifest.projects, options.only);
|
|
9562
|
+
let importedCount = 0;
|
|
9563
|
+
let skippedCount = 0;
|
|
9564
|
+
let unchangedCount = 0;
|
|
9565
|
+
for (const project of projects) {
|
|
9566
|
+
const projectRoot = expandPath(project.path);
|
|
9567
|
+
if (!await exists(projectRoot)) {
|
|
9568
|
+
console.log(`WARN project missing: ${project.name} (${projectRoot})`);
|
|
9569
|
+
continue;
|
|
9275
9570
|
}
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
|
|
9283
|
-
continue;
|
|
9284
|
-
}
|
|
9285
|
-
await visit(full, depth + 1);
|
|
9286
|
-
continue;
|
|
9287
|
-
}
|
|
9288
|
-
if (!entry.isFile()) {
|
|
9571
|
+
const candidates = await collectSeedCandidates(project, projectRoot, ignorePatterns);
|
|
9572
|
+
for (const candidate of candidates) {
|
|
9573
|
+
const fileStat = await statSeedFile(candidate.filePath);
|
|
9574
|
+
const recorded = state.files[candidate.destinationUri];
|
|
9575
|
+
if (fileStat && recorded && recorded.mtimeMs === fileStat.mtimeMs && recorded.size === fileStat.size) {
|
|
9576
|
+
unchangedCount += 1;
|
|
9289
9577
|
continue;
|
|
9290
9578
|
}
|
|
9291
|
-
|
|
9579
|
+
const importPath = await prepareSeedFile(config, candidate, options.dryRun === true);
|
|
9580
|
+
if (!importPath) {
|
|
9581
|
+
skippedCount += 1;
|
|
9292
9582
|
continue;
|
|
9293
9583
|
}
|
|
9294
|
-
|
|
9295
|
-
|
|
9584
|
+
const args = withIdentity(config, ["add-resource", importPath, "--to", candidate.destinationUri, "--wait"]);
|
|
9585
|
+
await maybeRun(options.dryRun === true, ov, args);
|
|
9586
|
+
importedCount += 1;
|
|
9587
|
+
if (fileStat && options.dryRun !== true) {
|
|
9588
|
+
state.files[candidate.destinationUri] = { mtimeMs: fileStat.mtimeMs, size: fileStat.size };
|
|
9296
9589
|
}
|
|
9297
|
-
out.push(full);
|
|
9298
9590
|
}
|
|
9299
9591
|
}
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
}
|
|
9303
|
-
function workfileToVikingUri(config, team, filePath) {
|
|
9304
|
-
const rel = (0, import_node_path9.relative)(team.worktree, filePath).split(import_node_path9.sep).join("/");
|
|
9305
|
-
return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
9306
|
-
}
|
|
9307
|
-
function isInSharedNamespace(config, uri) {
|
|
9308
|
-
return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`);
|
|
9309
|
-
}
|
|
9310
|
-
function isInTeamNamespace(config, uri, team) {
|
|
9311
|
-
return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`);
|
|
9312
|
-
}
|
|
9313
|
-
function sharedUriFor(config, personalUri, team) {
|
|
9314
|
-
const prefix = `viking://user/${uriSegment(config.user)}/memories/`;
|
|
9315
|
-
if (!personalUri.startsWith(prefix)) {
|
|
9316
|
-
throw new Error(`Refusing to publish memory outside the current user namespace: ${personalUri}`);
|
|
9592
|
+
if (options.dryRun !== true) {
|
|
9593
|
+
await writeSeedState(statePath, state);
|
|
9317
9594
|
}
|
|
9318
|
-
|
|
9319
|
-
|
|
9595
|
+
console.log(
|
|
9596
|
+
`Seed complete: ${importedCount} candidate(s), ${unchangedCount} unchanged, ${skippedCount} skipped for safety.`
|
|
9597
|
+
);
|
|
9320
9598
|
}
|
|
9321
|
-
function
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
throw new Error(`Refusing to unpublish a URI outside team "${team}" shared namespace: ${sharedUri}`);
|
|
9599
|
+
function filterProjects(projects, only) {
|
|
9600
|
+
if (!only || only.length === 0) {
|
|
9601
|
+
return projects;
|
|
9325
9602
|
}
|
|
9326
|
-
const
|
|
9327
|
-
|
|
9603
|
+
const known = new Set(projects.map((project) => project.name));
|
|
9604
|
+
const missing = only.filter((name) => !known.has(name));
|
|
9605
|
+
if (missing.length > 0) {
|
|
9606
|
+
const all = [...known].join(", ");
|
|
9607
|
+
throw new Error(`Unknown project(s) in --only: ${missing.join(", ")}. Manifest projects: ${all}`);
|
|
9608
|
+
}
|
|
9609
|
+
const want = new Set(only);
|
|
9610
|
+
return projects.filter((project) => want.has(project.name));
|
|
9328
9611
|
}
|
|
9329
|
-
function
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9612
|
+
async function statSeedFile(path) {
|
|
9613
|
+
try {
|
|
9614
|
+
const result = await (0, import_promises8.stat)(path);
|
|
9615
|
+
return { mtimeMs: result.mtimeMs, size: result.size };
|
|
9616
|
+
} catch (_err) {
|
|
9617
|
+
return void 0;
|
|
9333
9618
|
}
|
|
9334
|
-
return uri.slice(prefix.length);
|
|
9335
9619
|
}
|
|
9336
|
-
function
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
if (
|
|
9341
|
-
|
|
9342
|
-
break;
|
|
9620
|
+
async function readSeedState(path) {
|
|
9621
|
+
try {
|
|
9622
|
+
const raw = await (0, import_promises8.readFile)(path, "utf8");
|
|
9623
|
+
const parsed = JSON.parse(raw);
|
|
9624
|
+
if (parsed.version !== 1 || !isJsonObject(parsed.files)) {
|
|
9625
|
+
return { files: {}, version: 1 };
|
|
9343
9626
|
}
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9627
|
+
const files = {};
|
|
9628
|
+
for (const [uri, entry] of Object.entries(parsed.files)) {
|
|
9629
|
+
if (isJsonObject(entry) && typeof entry.mtimeMs === "number" && typeof entry.size === "number") {
|
|
9630
|
+
files[uri] = { mtimeMs: entry.mtimeMs, size: entry.size };
|
|
9631
|
+
}
|
|
9349
9632
|
}
|
|
9350
|
-
|
|
9351
|
-
}
|
|
9352
|
-
|
|
9353
|
-
cleaned.push(lines[index]);
|
|
9633
|
+
return { files, version: 1 };
|
|
9634
|
+
} catch (_err) {
|
|
9635
|
+
return { files: {}, version: 1 };
|
|
9354
9636
|
}
|
|
9355
|
-
return cleaned.join("\n");
|
|
9356
9637
|
}
|
|
9357
|
-
async function
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
return "<dry-run memory body>";
|
|
9362
|
-
}
|
|
9363
|
-
const result = await runCommand(ov, args);
|
|
9364
|
-
if (!result.stdout.trim()) {
|
|
9365
|
-
throw new Error(`Refusing to publish empty memory at ${uri}`);
|
|
9366
|
-
}
|
|
9367
|
-
return result.stdout;
|
|
9638
|
+
async function writeSeedState(path, state) {
|
|
9639
|
+
await ensureDirectory((0, import_node_path9.dirname)(path), false);
|
|
9640
|
+
await (0, import_promises8.writeFile)(path, `${JSON.stringify(state, void 0, 2)}
|
|
9641
|
+
`, { encoding: "utf8", mode: 384 });
|
|
9368
9642
|
}
|
|
9369
|
-
async function
|
|
9370
|
-
const
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9643
|
+
async function runInitManifest(config, options) {
|
|
9644
|
+
const manifestPath = expandPath(
|
|
9645
|
+
options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path9.join)(config.agentContextHome, USER_MANIFEST_NAME)
|
|
9646
|
+
);
|
|
9647
|
+
const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
|
|
9648
|
+
const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
|
|
9649
|
+
const existingProjects = existingManifest?.projects ?? [];
|
|
9650
|
+
const projects = [...existingProjects];
|
|
9651
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9652
|
+
for (const project of existingProjects) {
|
|
9653
|
+
seen.add(await projectIdentity(project.path));
|
|
9654
|
+
}
|
|
9655
|
+
for (const repoInput of repoInputs) {
|
|
9656
|
+
const repoRoot = await resolveRepoRoot(repoInput);
|
|
9657
|
+
const identity = await projectIdentity(repoRoot);
|
|
9658
|
+
if (seen.has(identity)) {
|
|
9659
|
+
console.log(`Already in manifest: ${repoRoot}`);
|
|
9379
9660
|
continue;
|
|
9380
9661
|
}
|
|
9381
|
-
|
|
9662
|
+
seen.add(identity);
|
|
9663
|
+
projects.push(projectManifestForRepo(repoRoot, projects));
|
|
9382
9664
|
}
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9665
|
+
const outputManifest = {
|
|
9666
|
+
version: 1,
|
|
9667
|
+
projects: projects.map((project) => ({
|
|
9668
|
+
name: project.name,
|
|
9669
|
+
path: project.path,
|
|
9670
|
+
uri: project.uri,
|
|
9671
|
+
seed: [...project.seed]
|
|
9672
|
+
}))
|
|
9673
|
+
};
|
|
9674
|
+
if (existingManifest?.futureMonorepo) {
|
|
9675
|
+
const futureMonorepoKey = "future_monorepo";
|
|
9676
|
+
const pathCandidatesKey = "path_candidates";
|
|
9677
|
+
outputManifest[futureMonorepoKey] = {
|
|
9678
|
+
[pathCandidatesKey]: [...existingManifest.futureMonorepo.pathCandidates],
|
|
9679
|
+
uri: existingManifest.futureMonorepo.uri
|
|
9680
|
+
};
|
|
9392
9681
|
}
|
|
9393
|
-
const
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9682
|
+
const output2 = jsYaml.dump(outputManifest, { lineWidth: 120, noRefs: true });
|
|
9683
|
+
if (options.dryRun === true) {
|
|
9684
|
+
console.log(`# Would write ${manifestPath}`);
|
|
9685
|
+
console.log(output2.trimEnd());
|
|
9686
|
+
return;
|
|
9397
9687
|
}
|
|
9398
|
-
|
|
9688
|
+
await ensureDirectory((0, import_node_path9.dirname)(manifestPath), false);
|
|
9689
|
+
await (0, import_promises8.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
9690
|
+
await (0, import_promises8.chmod)(manifestPath, 384);
|
|
9691
|
+
console.log(`Wrote manifest: ${manifestPath}`);
|
|
9692
|
+
console.log("Seed with:");
|
|
9693
|
+
console.log(" threadnote seed --dry-run");
|
|
9694
|
+
console.log(" threadnote seed");
|
|
9399
9695
|
}
|
|
9400
|
-
async function
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9696
|
+
async function runSeedSkills(config, options) {
|
|
9697
|
+
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
9698
|
+
const skills = await collectSkillCandidates(config);
|
|
9699
|
+
const nativeMode = options.native === true;
|
|
9700
|
+
console.log(
|
|
9701
|
+
nativeMode ? "Skill seed mode: native OpenViking skills. This requires a working VLM config." : "Skill seed mode: resource catalog. Use --native only after configuring a working VLM provider."
|
|
9702
|
+
);
|
|
9703
|
+
for (const skill of skills) {
|
|
9704
|
+
console.log(`Skill ${skill.source}: ${skill.filePath}`);
|
|
9705
|
+
const args = nativeMode ? ["add-skill", skill.filePath, "--wait"] : ["add-resource", skill.filePath, "--to", skillResourceUri(skill), "--wait"];
|
|
9706
|
+
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
9707
|
+
}
|
|
9708
|
+
console.log(`Skill seed complete: ${skills.length} unique skill(s).`);
|
|
9709
|
+
}
|
|
9710
|
+
async function resolveRepoRoot(repoInput) {
|
|
9711
|
+
const inputPath = expandPath(repoInput);
|
|
9712
|
+
if (!await isDirectory(inputPath)) {
|
|
9713
|
+
throw new Error(`Repo path is not a directory: ${inputPath}`);
|
|
9415
9714
|
}
|
|
9416
|
-
|
|
9417
|
-
|
|
9715
|
+
return await gitValue(["rev-parse", "--show-toplevel"], inputPath) ?? inputPath;
|
|
9716
|
+
}
|
|
9717
|
+
async function projectIdentity(path) {
|
|
9718
|
+
const expanded = expandPath(path);
|
|
9418
9719
|
try {
|
|
9419
|
-
await (0, import_promises8.
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
await (0, import_promises8.rm)(stagingDir, { force: true, recursive: true });
|
|
9720
|
+
return await (0, import_promises8.realpath)(expanded);
|
|
9721
|
+
} catch (_err) {
|
|
9722
|
+
return expanded;
|
|
9423
9723
|
}
|
|
9424
9724
|
}
|
|
9425
|
-
|
|
9426
|
-
const
|
|
9427
|
-
const
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
if (
|
|
9450
|
-
|
|
9451
|
-
}
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
if (!isTransientOvFailure(result.stderr, result.stdout) || attempt === maxAttempts - 1) {
|
|
9460
|
-
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
9725
|
+
function projectManifestForRepo(repoRoot, existingProjects) {
|
|
9726
|
+
const baseName = uriSegment((0, import_node_path9.basename)(repoRoot));
|
|
9727
|
+
const usedNames = new Set(existingProjects.map((project) => project.name));
|
|
9728
|
+
const usedUris = new Set(existingProjects.map((project) => project.uri));
|
|
9729
|
+
let name = baseName;
|
|
9730
|
+
let uri = `viking://resources/repos/${name}`;
|
|
9731
|
+
if (usedNames.has(name) || usedUris.has(uri)) {
|
|
9732
|
+
name = `${baseName}-${sha256(repoRoot).slice(0, 8)}`;
|
|
9733
|
+
uri = `viking://resources/repos/${name}`;
|
|
9734
|
+
}
|
|
9735
|
+
return {
|
|
9736
|
+
name,
|
|
9737
|
+
path: portablePath(repoRoot),
|
|
9738
|
+
seed: DEFAULT_SEED_PATTERNS,
|
|
9739
|
+
uri
|
|
9740
|
+
};
|
|
9741
|
+
}
|
|
9742
|
+
async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
|
|
9743
|
+
const candidates = [];
|
|
9744
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9745
|
+
for (const pattern of project.seed) {
|
|
9746
|
+
const files = await resolveProjectPattern(projectRoot, pattern);
|
|
9747
|
+
for (const filePath of files) {
|
|
9748
|
+
const relativePath = toPosixPath((0, import_node_path9.relative)(projectRoot, filePath));
|
|
9749
|
+
if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
|
|
9750
|
+
continue;
|
|
9751
|
+
}
|
|
9752
|
+
seen.add(relativePath);
|
|
9753
|
+
candidates.push({
|
|
9754
|
+
destinationUri: `${trimTrailingSlash(project.uri)}/${relativePath}`,
|
|
9755
|
+
filePath,
|
|
9756
|
+
projectName: project.name,
|
|
9757
|
+
relativePath
|
|
9758
|
+
});
|
|
9461
9759
|
}
|
|
9462
|
-
await sleep(1e3 * (attempt + 1));
|
|
9463
9760
|
}
|
|
9761
|
+
return candidates;
|
|
9464
9762
|
}
|
|
9465
|
-
async function
|
|
9466
|
-
const
|
|
9467
|
-
if (
|
|
9468
|
-
|
|
9763
|
+
async function resolveProjectPattern(projectRoot, pattern) {
|
|
9764
|
+
const normalizedPattern = toPosixPath(pattern);
|
|
9765
|
+
if (!hasGlob(normalizedPattern)) {
|
|
9766
|
+
const filePath = (0, import_node_path9.join)(projectRoot, normalizedPattern);
|
|
9767
|
+
return await isFile(filePath) ? [filePath] : [];
|
|
9469
9768
|
}
|
|
9470
|
-
|
|
9471
|
-
|
|
9769
|
+
const globBase = getGlobBase(normalizedPattern);
|
|
9770
|
+
const basePath = (0, import_node_path9.join)(projectRoot, globBase);
|
|
9771
|
+
if (!await exists(basePath)) {
|
|
9772
|
+
return [];
|
|
9472
9773
|
}
|
|
9774
|
+
const regex = globToRegExp(normalizedPattern);
|
|
9775
|
+
const files = await walkFiles(basePath);
|
|
9776
|
+
return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path9.relative)(projectRoot, filePath))));
|
|
9473
9777
|
}
|
|
9474
|
-
function
|
|
9475
|
-
const
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
await writeMemoryFile(config, ov, uri, content, initialMode, false);
|
|
9482
|
-
}
|
|
9483
|
-
async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
|
|
9484
|
-
try {
|
|
9485
|
-
await removeMemoryUri(config, ov, sourceUri, dryRun);
|
|
9486
|
-
} catch (sourceErr) {
|
|
9487
|
-
if (dryRun) {
|
|
9488
|
-
throw sourceErr;
|
|
9489
|
-
}
|
|
9490
|
-
console.error(
|
|
9491
|
-
`Source removal failed during ${label}; rolling back ${rollbackUri} so the system is back to the pre-${label} state.`
|
|
9778
|
+
async function prepareSeedFile(config, candidate, dryRun) {
|
|
9779
|
+
const content = await (0, import_promises8.readFile)(candidate.filePath, "utf8");
|
|
9780
|
+
const redactedContent = shouldRedactPath(candidate.relativePath) ? redactContent(candidate.relativePath, content) : content;
|
|
9781
|
+
const secretMatches = detectSecretMatches(redactedContent);
|
|
9782
|
+
if (secretMatches.length > 0) {
|
|
9783
|
+
console.log(
|
|
9784
|
+
`SKIP ${candidate.projectName}/${candidate.relativePath}: possible secret (${secretMatches.slice(0, MAX_SECRET_MATCHES_TO_PRINT).join(", ")})`
|
|
9492
9785
|
);
|
|
9493
|
-
|
|
9494
|
-
await removeMemoryUri(config, ov, rollbackUri, false);
|
|
9495
|
-
} catch (rollbackErr) {
|
|
9496
|
-
console.error(
|
|
9497
|
-
`Rollback of ${rollbackUri} also failed. Manual cleanup needed via: threadnote forget ${rollbackUri}
|
|
9498
|
-
Rollback error: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`
|
|
9499
|
-
);
|
|
9500
|
-
}
|
|
9501
|
-
await bestEffortRemoveWorktreeFile(rollbackUri, worktree, label);
|
|
9502
|
-
throw sourceErr;
|
|
9503
|
-
}
|
|
9504
|
-
}
|
|
9505
|
-
async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
9506
|
-
if (label !== "publish") {
|
|
9507
|
-
return;
|
|
9508
|
-
}
|
|
9509
|
-
const prefix = "viking://";
|
|
9510
|
-
if (!rollbackUri.startsWith(prefix)) {
|
|
9511
|
-
return;
|
|
9786
|
+
return void 0;
|
|
9512
9787
|
}
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
if (sharedIndex === -1 || sharedIndex + 2 >= parts.length) {
|
|
9516
|
-
return;
|
|
9788
|
+
if (redactedContent === content) {
|
|
9789
|
+
return candidate.filePath;
|
|
9517
9790
|
}
|
|
9518
|
-
const
|
|
9519
|
-
if (
|
|
9520
|
-
|
|
9791
|
+
const redactedPath = (0, import_node_path9.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
|
|
9792
|
+
if (dryRun) {
|
|
9793
|
+
console.log(`Would write redacted copy: ${redactedPath}`);
|
|
9794
|
+
return redactedPath;
|
|
9521
9795
|
}
|
|
9522
|
-
await (
|
|
9796
|
+
await ensureDirectory((0, import_node_path9.dirname)(redactedPath), false);
|
|
9797
|
+
await (0, import_promises8.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
|
|
9798
|
+
await (0, import_promises8.chmod)(redactedPath, 384);
|
|
9799
|
+
return redactedPath;
|
|
9523
9800
|
}
|
|
9524
|
-
async function
|
|
9525
|
-
const
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9801
|
+
async function collectSkillCandidates(config) {
|
|
9802
|
+
const sources = [
|
|
9803
|
+
{ pattern: "~/.codex/skills/**/SKILL.md", source: "codex-global" },
|
|
9804
|
+
{ pattern: "~/.codex/plugins/cache/**/skills/**/SKILL.md", source: "codex-plugin-cache" },
|
|
9805
|
+
{ pattern: "~/.claude/skills/**/SKILL.md", source: "claude-global" }
|
|
9806
|
+
];
|
|
9807
|
+
try {
|
|
9808
|
+
const manifest = await readSeedManifest(config.manifestPath);
|
|
9809
|
+
for (const project of manifest.projects) {
|
|
9810
|
+
sources.push({
|
|
9811
|
+
pattern: `${project.path}/.claude/skills/**/SKILL.md`,
|
|
9812
|
+
source: `repo-local:${project.name}`
|
|
9813
|
+
});
|
|
9814
|
+
}
|
|
9815
|
+
} catch (err) {
|
|
9816
|
+
console.log(`WARN cannot read manifest for repo-local skill discovery: ${errorMessage(err)}`);
|
|
9529
9817
|
}
|
|
9530
|
-
const
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9818
|
+
const seenHashes = /* @__PURE__ */ new Set();
|
|
9819
|
+
const skills = [];
|
|
9820
|
+
for (const source of sources) {
|
|
9821
|
+
const files = await resolveAbsolutePattern(expandPath(source.pattern));
|
|
9822
|
+
for (const filePath of files) {
|
|
9823
|
+
const content = await (0, import_promises8.readFile)(filePath, "utf8");
|
|
9824
|
+
const matches = detectSecretMatches(content);
|
|
9825
|
+
if (matches.length > 0) {
|
|
9826
|
+
console.log(`SKIP skill with possible secret: ${filePath}`);
|
|
9827
|
+
continue;
|
|
9536
9828
|
}
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9829
|
+
const hash = sha256(content);
|
|
9830
|
+
if (seenHashes.has(hash)) {
|
|
9831
|
+
continue;
|
|
9832
|
+
}
|
|
9833
|
+
seenHashes.add(hash);
|
|
9834
|
+
skills.push({ filePath, hash, source: source.source });
|
|
9541
9835
|
}
|
|
9542
|
-
await sleep(1e3 * (attempt + 1));
|
|
9543
9836
|
}
|
|
9837
|
+
return skills;
|
|
9544
9838
|
}
|
|
9545
|
-
async function
|
|
9546
|
-
const
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
async function hasUncommittedChanges(worktree) {
|
|
9550
|
-
const result = await runCommand("git", ["-C", worktree, "status", "--porcelain"], { allowFailure: true });
|
|
9551
|
-
return result.stdout.trim().length > 0;
|
|
9552
|
-
}
|
|
9553
|
-
async function gitOutput(worktree, args, dryRun) {
|
|
9554
|
-
if (dryRun) {
|
|
9555
|
-
return void 0;
|
|
9556
|
-
}
|
|
9557
|
-
const result = await runCommand("git", ["-C", worktree, ...args], { allowFailure: true });
|
|
9558
|
-
if (result.exitCode !== 0) {
|
|
9559
|
-
return void 0;
|
|
9839
|
+
async function resolveAbsolutePattern(pattern) {
|
|
9840
|
+
const normalizedPattern = toPosixPath(pattern);
|
|
9841
|
+
if (!hasGlob(normalizedPattern)) {
|
|
9842
|
+
return await isFile(normalizedPattern) ? [normalizedPattern] : [];
|
|
9560
9843
|
}
|
|
9561
|
-
|
|
9562
|
-
}
|
|
9563
|
-
|
|
9564
|
-
const result = await runCommand("git", ["-C", worktree, "diff", "--name-status", "-z", `${beforeRev}..${afterRev}`], {
|
|
9565
|
-
allowFailure: true
|
|
9566
|
-
});
|
|
9567
|
-
if (result.exitCode !== 0) {
|
|
9844
|
+
const globBase = getGlobBase(normalizedPattern);
|
|
9845
|
+
const basePath = globBase.startsWith("/") ? globBase : `/${globBase}`;
|
|
9846
|
+
if (!await exists(basePath)) {
|
|
9568
9847
|
return [];
|
|
9569
9848
|
}
|
|
9570
|
-
const
|
|
9571
|
-
const
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9849
|
+
const regex = globToRegExp(normalizedPattern);
|
|
9850
|
+
const files = await walkFiles(basePath);
|
|
9851
|
+
return files.filter((filePath) => regex.test(toPosixPath(filePath)));
|
|
9852
|
+
}
|
|
9853
|
+
function skillResourceUri(skill) {
|
|
9854
|
+
return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0, import_node_path9.basename)((0, import_node_path9.dirname)(skill.filePath)))}-${skill.hash.slice(0, 12)}.md`;
|
|
9855
|
+
}
|
|
9856
|
+
async function loadIgnorePatterns() {
|
|
9857
|
+
const raw = await (0, import_promises8.readFile)((0, import_node_path9.join)(toolRoot(), ".threadnoteignore"), "utf8");
|
|
9858
|
+
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
9859
|
+
}
|
|
9860
|
+
function matchesIgnore(relativePath, patterns) {
|
|
9861
|
+
const path = toPosixPath(relativePath);
|
|
9862
|
+
for (const pattern of patterns) {
|
|
9863
|
+
const normalizedPattern = toPosixPath(pattern);
|
|
9864
|
+
if (normalizedPattern.endsWith("/")) {
|
|
9865
|
+
const directory = normalizedPattern.slice(0, -1);
|
|
9866
|
+
if (path === directory || path.includes(`/${directory}/`) || path.startsWith(`${directory}/`)) {
|
|
9867
|
+
return true;
|
|
9581
9868
|
}
|
|
9582
|
-
index += 3;
|
|
9583
9869
|
continue;
|
|
9584
9870
|
}
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
9588
|
-
changes.push({ path: (0, import_node_path9.join)(worktree, rel), relativePath: rel, status });
|
|
9871
|
+
if (globToRegExp(normalizedPattern).test(path) || globToRegExp(`**/${normalizedPattern}`).test(path)) {
|
|
9872
|
+
return true;
|
|
9589
9873
|
}
|
|
9590
|
-
index += 2;
|
|
9591
9874
|
}
|
|
9592
|
-
return
|
|
9875
|
+
return false;
|
|
9593
9876
|
}
|
|
9594
|
-
|
|
9595
|
-
const
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
await removeMemoryUri(config, ov, uri, false);
|
|
9607
|
-
continue;
|
|
9608
|
-
}
|
|
9609
|
-
if (!await isFile(change.path)) {
|
|
9610
|
-
continue;
|
|
9877
|
+
function shouldRedactPath(relativePath) {
|
|
9878
|
+
const path = toPosixPath(relativePath);
|
|
9879
|
+
return path.endsWith(".mcp.json") || path.endsWith("config.toml") || path.includes("/settings") || path.includes("/settings.local");
|
|
9880
|
+
}
|
|
9881
|
+
function redactContent(relativePath, content) {
|
|
9882
|
+
if (relativePath.endsWith(".json")) {
|
|
9883
|
+
try {
|
|
9884
|
+
const parsed = JSON.parse(content);
|
|
9885
|
+
return `${JSON.stringify(redactJsonValue(parsed), null, 2)}
|
|
9886
|
+
`;
|
|
9887
|
+
} catch (_err) {
|
|
9888
|
+
return redactText(content);
|
|
9611
9889
|
}
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
|
|
9890
|
+
}
|
|
9891
|
+
return redactText(content);
|
|
9892
|
+
}
|
|
9893
|
+
function redactJsonValue(value) {
|
|
9894
|
+
if (Array.isArray(value)) {
|
|
9895
|
+
return value.map((item) => redactJsonValue(item));
|
|
9896
|
+
}
|
|
9897
|
+
if (!isJsonObject(value)) {
|
|
9898
|
+
return value;
|
|
9899
|
+
}
|
|
9900
|
+
const output2 = {};
|
|
9901
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
9902
|
+
output2[key] = isSensitiveKey(key) ? "[REDACTED]" : redactJsonValue(nestedValue);
|
|
9903
|
+
}
|
|
9904
|
+
return output2;
|
|
9905
|
+
}
|
|
9906
|
+
function isSensitiveKey(key) {
|
|
9907
|
+
return /token|secret|password|credential|authorization|api[_-]?key|client[_-]?secret|bearer/i.test(key);
|
|
9908
|
+
}
|
|
9909
|
+
function detectSecretMatches(content) {
|
|
9910
|
+
const detectors = [
|
|
9911
|
+
{ label: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
9912
|
+
{ label: "openai-key", regex: /sk-[A-Za-z0-9_-]{24,}/ },
|
|
9913
|
+
{ label: "github-token", regex: /gh[pousr]_[A-Za-z0-9_]{24,}/ },
|
|
9914
|
+
{ label: "slack-token", regex: /xox[abprs]-[A-Za-z0-9-]{24,}/ },
|
|
9915
|
+
{ label: "bearer-token", regex: /Bearer\s+[A-Za-z0-9._~+/=-]{24,}/i },
|
|
9916
|
+
{ label: "aws-access-key", regex: /AKIA[0-9A-Z]{16}/ }
|
|
9917
|
+
];
|
|
9918
|
+
const matches = [];
|
|
9919
|
+
for (const detector of detectors) {
|
|
9920
|
+
if (detector.regex.test(content)) {
|
|
9921
|
+
matches.push(detector.label);
|
|
9616
9922
|
}
|
|
9617
|
-
await ensureSharedDirectoryChain(config, ov, uri, false);
|
|
9618
|
-
const writeMode = ovHasResource ? "replace" : "create";
|
|
9619
|
-
await ingestSingleFile(ov, config, uri, change.path, writeMode);
|
|
9620
9923
|
}
|
|
9924
|
+
return matches;
|
|
9621
9925
|
}
|
|
9622
9926
|
|
|
9623
9927
|
// src/lifecycle.ts
|
|
@@ -11098,7 +11402,12 @@ async function main() {
|
|
|
11098
11402
|
).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
|
|
11099
11403
|
await runUninstall(getRuntimeConfig(program2), options);
|
|
11100
11404
|
});
|
|
11101
|
-
program2.command("seed").description("Seed curated context from the manifest; never indexes whole repos by default").option("--dry-run", "Print files and ov commands without importing").option("--manifest <path>", "Manifest path for this seed run").
|
|
11405
|
+
program2.command("seed").description("Seed curated context from the manifest; never indexes whole repos by default").option("--dry-run", "Print files and ov commands without importing").option("--force", "Re-upload every candidate even if mtime+size match the recorded state").option("--manifest <path>", "Manifest path for this seed run").option(
|
|
11406
|
+
"--only <project>",
|
|
11407
|
+
"Restrict seeding to one or more manifest projects by name; repeat for multiple",
|
|
11408
|
+
collectOption,
|
|
11409
|
+
[]
|
|
11410
|
+
).action(async (options) => {
|
|
11102
11411
|
await runSeed(getRuntimeConfig(program2, options.manifest), options);
|
|
11103
11412
|
});
|
|
11104
11413
|
program2.command("init-manifest").description("Create or update a per-developer seed manifest from one or more repo roots").option("--dry-run", "Print the manifest without writing it").option("--path <path>", "Manifest path; defaults to THREADNOTE_MANIFEST or ~/.openviking/seed-manifest.yaml").option("--replace", "Replace the manifest instead of merging with existing projects").option("--repo <path>", "Repo root to include; repeat for multiple repos", collectOption, []).action(async (options) => {
|