threadnote 0.7.4 → 0.7.6
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 +33 -10
- package/config/launchd/io.threadnote.openviking.plist.template +1 -1
- package/dist/mcp_server.cjs +776 -36
- package/dist/threadnote.cjs +938 -158
- package/docs/agent-instructions.md +24 -5
- package/docs/migration.md +4 -2
- package/package.json +10 -5
package/dist/threadnote.cjs
CHANGED
|
@@ -3524,7 +3524,7 @@ var DEFAULT_SEED_PATTERNS = [
|
|
|
3524
3524
|
|
|
3525
3525
|
// src/hooks.ts
|
|
3526
3526
|
var import_promises7 = require("node:fs/promises");
|
|
3527
|
-
var
|
|
3527
|
+
var import_node_path9 = require("node:path");
|
|
3528
3528
|
|
|
3529
3529
|
// src/mcp.ts
|
|
3530
3530
|
var import_node_fs2 = require("node:fs");
|
|
@@ -3850,6 +3850,27 @@ async function exists(path) {
|
|
|
3850
3850
|
return false;
|
|
3851
3851
|
}
|
|
3852
3852
|
}
|
|
3853
|
+
async function isExecutable(path) {
|
|
3854
|
+
try {
|
|
3855
|
+
await (0, import_promises.access)(path, import_node_fs.constants.X_OK);
|
|
3856
|
+
return true;
|
|
3857
|
+
} catch (_err) {
|
|
3858
|
+
return false;
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
function suggestedShellRc(shellPath, currentPlatform) {
|
|
3862
|
+
const shell = shellPath ?? "";
|
|
3863
|
+
if (shell.endsWith("/zsh")) {
|
|
3864
|
+
return "~/.zshrc";
|
|
3865
|
+
}
|
|
3866
|
+
if (shell.endsWith("/bash")) {
|
|
3867
|
+
return currentPlatform === "darwin" ? "~/.bash_profile" : "~/.bashrc";
|
|
3868
|
+
}
|
|
3869
|
+
if (shell.endsWith("/fish")) {
|
|
3870
|
+
return "~/.config/fish/config.fish";
|
|
3871
|
+
}
|
|
3872
|
+
return "your shell rc";
|
|
3873
|
+
}
|
|
3853
3874
|
async function isFile(path) {
|
|
3854
3875
|
try {
|
|
3855
3876
|
return (await (0, import_promises.stat)(path)).isFile();
|
|
@@ -3937,6 +3958,57 @@ function portablePath(path) {
|
|
|
3937
3958
|
function getInvocationCwd() {
|
|
3938
3959
|
return process.env.THREADNOTE_CALLER_CWD ?? process.cwd();
|
|
3939
3960
|
}
|
|
3961
|
+
function recallQueryRequestsWorkspaceContext(query) {
|
|
3962
|
+
const normalized = query.toLowerCase();
|
|
3963
|
+
return /\b(?:this|current)\s+(?:branch|repo|repository|workspace|worktree)\b/.test(normalized);
|
|
3964
|
+
}
|
|
3965
|
+
async function enrichRecallQueryWithWorkspaceContext(query, options = {}) {
|
|
3966
|
+
return enrichRecallQueryWithWorkspaceTerms(query, options, true);
|
|
3967
|
+
}
|
|
3968
|
+
async function enrichRecallQueryWithWorkspaceProjectContext(query, options = {}) {
|
|
3969
|
+
return enrichRecallQueryWithWorkspaceTerms(query, options, false);
|
|
3970
|
+
}
|
|
3971
|
+
async function enrichRecallQueryWithWorkspaceTerms(query, options, includeBranch) {
|
|
3972
|
+
if (!recallQueryRequestsWorkspaceContext(query)) {
|
|
3973
|
+
return query;
|
|
3974
|
+
}
|
|
3975
|
+
const terms = await currentWorkspaceRecallTerms(options, includeBranch);
|
|
3976
|
+
const additions = terms.filter((term) => !query.toLowerCase().includes(term.toLowerCase()));
|
|
3977
|
+
return additions.length > 0 ? `${query} ${additions.join(" ")}` : query;
|
|
3978
|
+
}
|
|
3979
|
+
async function currentWorkspaceRecallTerms(options, includeBranch) {
|
|
3980
|
+
const cwd = options.cwd ?? (options.includeProcessCwd === false ? void 0 : getInvocationCwd());
|
|
3981
|
+
if (!cwd || !(0, import_node_path.isAbsolute)(cwd)) {
|
|
3982
|
+
return [];
|
|
3983
|
+
}
|
|
3984
|
+
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
|
|
3985
|
+
if (!repoRoot) {
|
|
3986
|
+
return [];
|
|
3987
|
+
}
|
|
3988
|
+
const branch = await gitValue(["branch", "--show-current"], repoRoot);
|
|
3989
|
+
const parent = (0, import_node_path.dirname)(repoRoot);
|
|
3990
|
+
return uniqueUsefulWorkspaceTerms([
|
|
3991
|
+
{ source: "branch", value: includeBranch ? branch : void 0 },
|
|
3992
|
+
{ source: "path", value: (0, import_node_path.basename)(repoRoot) },
|
|
3993
|
+
{ source: "path", value: parent === (0, import_node_os.homedir)() ? void 0 : (0, import_node_path.basename)(parent) }
|
|
3994
|
+
]);
|
|
3995
|
+
}
|
|
3996
|
+
function uniqueUsefulWorkspaceTerms(values) {
|
|
3997
|
+
const ignored = /* @__PURE__ */ new Set(["repos", "repositories", "workspaces", "worktrees"]);
|
|
3998
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3999
|
+
const terms = [];
|
|
4000
|
+
for (const { source, value } of values) {
|
|
4001
|
+
const term = value?.trim();
|
|
4002
|
+
const normalized = term?.toLowerCase();
|
|
4003
|
+
const tooShort = source === "branch" ? false : (term?.length ?? 0) < 4;
|
|
4004
|
+
if (!term || !normalized || tooShort || ignored.has(normalized) || seen.has(normalized)) {
|
|
4005
|
+
continue;
|
|
4006
|
+
}
|
|
4007
|
+
seen.add(normalized);
|
|
4008
|
+
terms.push(term);
|
|
4009
|
+
}
|
|
4010
|
+
return terms;
|
|
4011
|
+
}
|
|
3940
4012
|
function toPosixPath(path) {
|
|
3941
4013
|
return path.split(import_node_path.sep).join("/");
|
|
3942
4014
|
}
|
|
@@ -3960,19 +4032,29 @@ function exactRecallTerms(query) {
|
|
|
3960
4032
|
"branch",
|
|
3961
4033
|
"case",
|
|
3962
4034
|
"current",
|
|
4035
|
+
"durable",
|
|
3963
4036
|
"find",
|
|
4037
|
+
"feature",
|
|
4038
|
+
"features",
|
|
3964
4039
|
"handoff",
|
|
3965
4040
|
"issue",
|
|
3966
4041
|
"issues",
|
|
4042
|
+
"knowledge",
|
|
3967
4043
|
"latest",
|
|
3968
4044
|
"memory",
|
|
3969
4045
|
"memories",
|
|
4046
|
+
"project",
|
|
3970
4047
|
"recall",
|
|
4048
|
+
"repo",
|
|
4049
|
+
"repository",
|
|
3971
4050
|
"related",
|
|
3972
4051
|
"search",
|
|
3973
|
-
"
|
|
4052
|
+
"stored",
|
|
3974
4053
|
"this",
|
|
3975
|
-
"
|
|
4054
|
+
"the",
|
|
4055
|
+
"with",
|
|
4056
|
+
"workspace",
|
|
4057
|
+
"worktree"
|
|
3976
4058
|
]);
|
|
3977
4059
|
const seen = /* @__PURE__ */ new Set();
|
|
3978
4060
|
const terms = [];
|
|
@@ -4520,7 +4602,7 @@ function existsSyncDirectory(path) {
|
|
|
4520
4602
|
|
|
4521
4603
|
// src/memory.ts
|
|
4522
4604
|
var import_promises5 = require("node:fs/promises");
|
|
4523
|
-
var
|
|
4605
|
+
var import_node_path6 = require("node:path");
|
|
4524
4606
|
|
|
4525
4607
|
// src/manifest.ts
|
|
4526
4608
|
var import_promises3 = require("node:fs/promises");
|
|
@@ -7218,10 +7300,445 @@ function readStringArray(object, key) {
|
|
|
7218
7300
|
return value;
|
|
7219
7301
|
}
|
|
7220
7302
|
|
|
7303
|
+
// src/memory_hygiene.ts
|
|
7304
|
+
var import_node_path3 = require("node:path");
|
|
7305
|
+
var HYGIENE_SOURCES_HEADING = "## Threadnote Hygiene Sources";
|
|
7306
|
+
var STALE_HANDOFF_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
7307
|
+
function parseMemoryDocument(uri, content) {
|
|
7308
|
+
const trimmed = content.trim();
|
|
7309
|
+
if (!trimmed) {
|
|
7310
|
+
return void 0;
|
|
7311
|
+
}
|
|
7312
|
+
const separatorIndex = trimmed.indexOf("\n\n");
|
|
7313
|
+
const header = separatorIndex === -1 ? trimmed : trimmed.slice(0, separatorIndex);
|
|
7314
|
+
const body = separatorIndex === -1 ? "" : trimmed.slice(separatorIndex + 2).trim();
|
|
7315
|
+
const firstLine2 = header.split("\n")[0]?.trim();
|
|
7316
|
+
if (firstLine2 !== "MEMORY" && firstLine2 !== "HANDOFF") {
|
|
7317
|
+
return void 0;
|
|
7318
|
+
}
|
|
7319
|
+
const kind = parseOptionalMemoryKind(headerValue(header, "kind")) ?? (firstLine2 === "HANDOFF" ? "handoff" : void 0);
|
|
7320
|
+
const status = parseOptionalMemoryStatus(headerValue(header, "status")) ?? "active";
|
|
7321
|
+
if (!kind) {
|
|
7322
|
+
return void 0;
|
|
7323
|
+
}
|
|
7324
|
+
return {
|
|
7325
|
+
body,
|
|
7326
|
+
content: trimmed,
|
|
7327
|
+
headerTitle: firstLine2,
|
|
7328
|
+
metadata: {
|
|
7329
|
+
archivedFrom: headerValue(header, "archived_from"),
|
|
7330
|
+
kind,
|
|
7331
|
+
project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
|
|
7332
|
+
sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
|
|
7333
|
+
status,
|
|
7334
|
+
supersedes: headerValue(header, "supersedes"),
|
|
7335
|
+
timestamp: headerValue(header, "timestamp") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
7336
|
+
topic: normalizeOptionalMetadata(headerValue(header, "topic"))
|
|
7337
|
+
},
|
|
7338
|
+
uri
|
|
7339
|
+
};
|
|
7340
|
+
}
|
|
7341
|
+
function buildCompactPlan(records, options) {
|
|
7342
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
7343
|
+
const groupedRecords = records.map((record) => groupableRecord(record)).filter((item) => item !== void 0).filter((item) => item.project === options.project).filter((item) => options.topic === void 0 || item.topic === options.topic).filter((item) => options.kind === void 0 || item.record.metadata.kind === options.kind);
|
|
7344
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7345
|
+
for (const item of groupedRecords) {
|
|
7346
|
+
groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
|
|
7347
|
+
}
|
|
7348
|
+
const keepUpdates = [];
|
|
7349
|
+
const archives = [];
|
|
7350
|
+
const forgets = [];
|
|
7351
|
+
const manualReview = [];
|
|
7352
|
+
for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
|
|
7353
|
+
const recordsInGroup = group.map((item) => item.record);
|
|
7354
|
+
const kind = recordsInGroup[0]?.metadata.kind;
|
|
7355
|
+
const topic = group[0]?.topic;
|
|
7356
|
+
const project = group[0]?.project;
|
|
7357
|
+
if (!kind || !project || !isCompactableKind(kind)) {
|
|
7358
|
+
continue;
|
|
7359
|
+
}
|
|
7360
|
+
const duplicateGroups = groupBy(recordsInGroup, (record) => comparableMemoryBody(record.body));
|
|
7361
|
+
const duplicateForgetUris = /* @__PURE__ */ new Set();
|
|
7362
|
+
const distinctBodyCount = duplicateGroups.size;
|
|
7363
|
+
for (const duplicateGroup of duplicateGroups.values()) {
|
|
7364
|
+
if (duplicateGroup.length < 2) {
|
|
7365
|
+
continue;
|
|
7366
|
+
}
|
|
7367
|
+
const duplicateKeep = preferredKeepRecord(duplicateGroup, topic);
|
|
7368
|
+
for (const duplicate of sortedNewestFirst(duplicateGroup).filter((record) => record.uri !== duplicateKeep.uri)) {
|
|
7369
|
+
duplicateForgetUris.add(duplicate.uri);
|
|
7370
|
+
forgets.push({ reason: `exact duplicate of ${duplicateKeep.uri}`, uri: duplicate.uri });
|
|
7371
|
+
}
|
|
7372
|
+
if (distinctBodyCount === 1 || kind !== "handoff") {
|
|
7373
|
+
keepUpdates.push({
|
|
7374
|
+
content: memoryContentWithHygieneSources(
|
|
7375
|
+
duplicateKeep,
|
|
7376
|
+
duplicateGroup.map((record) => record.uri)
|
|
7377
|
+
),
|
|
7378
|
+
reason: "keep exact duplicate group with source URIs",
|
|
7379
|
+
sourceUris: duplicateGroup.map((record) => record.uri),
|
|
7380
|
+
uri: duplicateKeep.uri
|
|
7381
|
+
});
|
|
7382
|
+
}
|
|
7383
|
+
}
|
|
7384
|
+
const remainingRecords = recordsInGroup.filter((record) => !duplicateForgetUris.has(record.uri));
|
|
7385
|
+
if (recordsInGroup.length > 1 && distinctBodyCount === 1) {
|
|
7386
|
+
continue;
|
|
7387
|
+
}
|
|
7388
|
+
if (remainingRecords.length === 0) {
|
|
7389
|
+
continue;
|
|
7390
|
+
}
|
|
7391
|
+
if (remainingRecords.length === 1) {
|
|
7392
|
+
const [record] = remainingRecords;
|
|
7393
|
+
if (!record) {
|
|
7394
|
+
continue;
|
|
7395
|
+
}
|
|
7396
|
+
if (record.metadata.supersedes === record.uri) {
|
|
7397
|
+
keepUpdates.push({
|
|
7398
|
+
content: memoryContentWithHygieneSources(record, [record.uri]),
|
|
7399
|
+
reason: "strip self-supersedes header",
|
|
7400
|
+
sourceUris: [record.uri],
|
|
7401
|
+
uri: record.uri
|
|
7402
|
+
});
|
|
7403
|
+
}
|
|
7404
|
+
if (isStaleLookingHandoff(record, now)) {
|
|
7405
|
+
manualReview.push({ reason: "stale-looking active handoff", uri: record.uri });
|
|
7406
|
+
}
|
|
7407
|
+
continue;
|
|
7408
|
+
}
|
|
7409
|
+
if (kind === "handoff") {
|
|
7410
|
+
const keep = preferredKeepRecord(remainingRecords, topic);
|
|
7411
|
+
const sourceUris = recordsInGroup.map((record) => record.uri);
|
|
7412
|
+
keepUpdates.push({
|
|
7413
|
+
content: memoryContentWithHygieneSources(keep, sourceUris),
|
|
7414
|
+
reason: "keep latest handoff and preserve source URIs",
|
|
7415
|
+
sourceUris,
|
|
7416
|
+
uri: keep.uri
|
|
7417
|
+
});
|
|
7418
|
+
for (const record of sortedNewestFirst(remainingRecords).filter((item) => item.uri !== keep.uri)) {
|
|
7419
|
+
archives.push({
|
|
7420
|
+
kind,
|
|
7421
|
+
project,
|
|
7422
|
+
reason: `older handoff for ${project}/${topic ?? "unknown"}`,
|
|
7423
|
+
topic,
|
|
7424
|
+
uri: record.uri
|
|
7425
|
+
});
|
|
7426
|
+
}
|
|
7427
|
+
continue;
|
|
7428
|
+
}
|
|
7429
|
+
for (const record of sortedNewestFirst(remainingRecords)) {
|
|
7430
|
+
manualReview.push({ reason: `non-exact ${kind} memory in overlapping group`, uri: record.uri });
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
return {
|
|
7434
|
+
archives: dedupeByUri(archives),
|
|
7435
|
+
forgets: dedupeByUri(forgets),
|
|
7436
|
+
keepUpdates: dedupeByUri(keepUpdates),
|
|
7437
|
+
manualReview: dedupeByUri(manualReview),
|
|
7438
|
+
options,
|
|
7439
|
+
recordsScanned: groupedRecords.length
|
|
7440
|
+
};
|
|
7441
|
+
}
|
|
7442
|
+
function memoryContentWithHygieneSources(record, sourceUris) {
|
|
7443
|
+
const body = stripHygieneSources(record.body);
|
|
7444
|
+
const uniqueSourceUris = [...new Set(sourceUris)].sort();
|
|
7445
|
+
const metadata = {
|
|
7446
|
+
...record.metadata,
|
|
7447
|
+
supersedes: record.metadata.supersedes === record.uri ? void 0 : record.metadata.supersedes
|
|
7448
|
+
};
|
|
7449
|
+
return formatMemoryDocument(
|
|
7450
|
+
record.headerTitle,
|
|
7451
|
+
metadata,
|
|
7452
|
+
[body, "", HYGIENE_SOURCES_HEADING, "", ...uniqueSourceUris.map((uri) => `- ${uri}`)].join("\n")
|
|
7453
|
+
);
|
|
7454
|
+
}
|
|
7455
|
+
function formatCompactPlan(plan, options) {
|
|
7456
|
+
const scope = [
|
|
7457
|
+
`project ${plan.options.project}`,
|
|
7458
|
+
plan.options.topic ? `topic ${plan.options.topic}` : void 0,
|
|
7459
|
+
plan.options.kind ? `kind ${plan.options.kind}` : void 0
|
|
7460
|
+
].filter((item) => item !== void 0).join(", ");
|
|
7461
|
+
const lines = [
|
|
7462
|
+
`${options.apply ? "Applying" : "Dry-run"} memory hygiene plan for ${scope}`,
|
|
7463
|
+
`Records scanned: ${plan.recordsScanned}`,
|
|
7464
|
+
"",
|
|
7465
|
+
formatPlanSection(
|
|
7466
|
+
"Keep/update",
|
|
7467
|
+
plan.keepUpdates.map((action) => `${action.uri} (${action.reason}; sources: ${action.sourceUris.length})`)
|
|
7468
|
+
),
|
|
7469
|
+
formatPlanSection(
|
|
7470
|
+
"Archive old handoffs",
|
|
7471
|
+
plan.archives.map((action) => `${action.uri} (${action.reason})`)
|
|
7472
|
+
),
|
|
7473
|
+
formatPlanSection(
|
|
7474
|
+
"Forget exact duplicates",
|
|
7475
|
+
plan.forgets.map((action) => `${action.uri} (${action.reason})`)
|
|
7476
|
+
),
|
|
7477
|
+
formatPlanSection(
|
|
7478
|
+
"Manual review",
|
|
7479
|
+
plan.manualReview.map((item) => `${item.uri} (${item.reason})`)
|
|
7480
|
+
)
|
|
7481
|
+
];
|
|
7482
|
+
if (!options.apply) {
|
|
7483
|
+
lines.push("", "No changes made. Re-run with --apply to execute this plan.");
|
|
7484
|
+
}
|
|
7485
|
+
return lines.join("\n");
|
|
7486
|
+
}
|
|
7487
|
+
function recallHygieneNudges(text, options) {
|
|
7488
|
+
const activeUris = activePersonalMemoryUrisFromText(text, options.user);
|
|
7489
|
+
const nudges = [];
|
|
7490
|
+
const returnedUriSet = new Set(activeUris);
|
|
7491
|
+
const returnedRecords = options.records?.filter((record) => returnedUriSet.has(record.uri)).map((record) => groupableRecord(record)) ?? [];
|
|
7492
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7493
|
+
for (const item of returnedRecords) {
|
|
7494
|
+
if (!item) {
|
|
7495
|
+
continue;
|
|
7496
|
+
}
|
|
7497
|
+
groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
|
|
7498
|
+
}
|
|
7499
|
+
for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
|
|
7500
|
+
if (group.length < 3) {
|
|
7501
|
+
continue;
|
|
7502
|
+
}
|
|
7503
|
+
const first = group[0];
|
|
7504
|
+
if (!first || !isCompactableKind(first.record.metadata.kind)) {
|
|
7505
|
+
continue;
|
|
7506
|
+
}
|
|
7507
|
+
const topic = first.topic;
|
|
7508
|
+
if (!topic) {
|
|
7509
|
+
continue;
|
|
7510
|
+
}
|
|
7511
|
+
nudges.push(
|
|
7512
|
+
`${group.length} active ${memoryKindPlural(first.record.metadata.kind)} look overlapping for ${first.project}/${topic}; run compact_context({"project":"${first.project}","topic":"${topic}","dryRun":true}).`
|
|
7513
|
+
);
|
|
7514
|
+
}
|
|
7515
|
+
const projectCounts = /* @__PURE__ */ new Map();
|
|
7516
|
+
for (const uri of activeUris) {
|
|
7517
|
+
const parsed = parsePersonalMemoryUri(uri, options.user);
|
|
7518
|
+
if (!parsed || parsed.kind !== "handoff" || parsed.status !== "active") {
|
|
7519
|
+
continue;
|
|
7520
|
+
}
|
|
7521
|
+
projectCounts.set(parsed.project, (projectCounts.get(parsed.project) ?? 0) + 1);
|
|
7522
|
+
}
|
|
7523
|
+
for (const [project, count] of [...projectCounts.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
7524
|
+
if (count < 10) {
|
|
7525
|
+
continue;
|
|
7526
|
+
}
|
|
7527
|
+
nudges.push(
|
|
7528
|
+
`Many active handoffs surfaced for ${project}; run compact_context({"project":"${project}","dryRun":true}).`
|
|
7529
|
+
);
|
|
7530
|
+
}
|
|
7531
|
+
return [...new Set(nudges)];
|
|
7532
|
+
}
|
|
7533
|
+
function activePersonalMemoryUrisFromText(text, user) {
|
|
7534
|
+
const userSegment = uriSegment(user);
|
|
7535
|
+
const matches = text.matchAll(/viking:\/\/[^\s)]+/g);
|
|
7536
|
+
const uris = [];
|
|
7537
|
+
for (const match of matches) {
|
|
7538
|
+
const uri = match[0]?.replace(/[.,;:]+$/g, "");
|
|
7539
|
+
if (!uri || !parsePersonalMemoryUri(uri, userSegment)) {
|
|
7540
|
+
continue;
|
|
7541
|
+
}
|
|
7542
|
+
uris.push(uri);
|
|
7543
|
+
}
|
|
7544
|
+
return [...new Set(uris)];
|
|
7545
|
+
}
|
|
7546
|
+
function parsePersonalMemoryUri(uri, user) {
|
|
7547
|
+
const prefix = `viking://user/${uriSegment(user)}/memories/`;
|
|
7548
|
+
if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
|
|
7549
|
+
return void 0;
|
|
7550
|
+
}
|
|
7551
|
+
const rest = uri.slice(prefix.length);
|
|
7552
|
+
const parts = rest.split("/").filter(Boolean);
|
|
7553
|
+
if (parts.length < 4) {
|
|
7554
|
+
return void 0;
|
|
7555
|
+
}
|
|
7556
|
+
if (parts[0] === "handoffs" && parts[1] === "active" && parts[2] && parts[3]?.endsWith(".md")) {
|
|
7557
|
+
return { kind: "handoff", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
|
|
7558
|
+
}
|
|
7559
|
+
if (parts[0] === "durable" && parts[1] === "projects" && parts[2] && parts[3]?.endsWith(".md")) {
|
|
7560
|
+
return { kind: "durable", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
|
|
7561
|
+
}
|
|
7562
|
+
if (parts[0] === "incidents" && parts[1] === "active" && parts[2] && parts[3]?.endsWith(".md")) {
|
|
7563
|
+
return { kind: "incident", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
|
|
7564
|
+
}
|
|
7565
|
+
return void 0;
|
|
7566
|
+
}
|
|
7567
|
+
function handoffTopicForBranch(branch, options) {
|
|
7568
|
+
const topic = normalizeOptionalMetadata(options.topic);
|
|
7569
|
+
if (options.timestamped === true) {
|
|
7570
|
+
if (topic) {
|
|
7571
|
+
throw new Error("Cannot combine --timestamped with --topic.");
|
|
7572
|
+
}
|
|
7573
|
+
return void 0;
|
|
7574
|
+
}
|
|
7575
|
+
return topic ?? normalizeOptionalMetadata(branch) ?? "current";
|
|
7576
|
+
}
|
|
7577
|
+
function groupableRecord(record) {
|
|
7578
|
+
if (record.metadata.status !== "active" || !isCompactableKind(record.metadata.kind)) {
|
|
7579
|
+
return void 0;
|
|
7580
|
+
}
|
|
7581
|
+
const project = normalizeOptionalMetadata(record.metadata.project) ?? parseProjectFromUri(record.uri);
|
|
7582
|
+
if (!project) {
|
|
7583
|
+
return void 0;
|
|
7584
|
+
}
|
|
7585
|
+
const topic = topicForRecord(record);
|
|
7586
|
+
const groupKey = [record.metadata.kind, project, topic ?? record.uri].join("\0");
|
|
7587
|
+
return { groupKey, project, record, topic };
|
|
7588
|
+
}
|
|
7589
|
+
function topicForRecord(record) {
|
|
7590
|
+
return normalizeOptionalMetadata(record.metadata.topic) ?? normalizeOptionalMetadata(branchFromBody(record.body)) ?? topicFromUri(record.uri);
|
|
7591
|
+
}
|
|
7592
|
+
function branchFromBody(body) {
|
|
7593
|
+
const branch = /^branch:\s*(.+)$/m.exec(body)?.[1]?.trim();
|
|
7594
|
+
return branch?.split(/\s+/)[0]?.replace(/[.,;:]+$/g, "");
|
|
7595
|
+
}
|
|
7596
|
+
function topicFromUri(uri) {
|
|
7597
|
+
const name = (0, import_node_path3.basename)(uri).replace(/\.md$/, "");
|
|
7598
|
+
return name.startsWith("threadnote-") ? void 0 : name;
|
|
7599
|
+
}
|
|
7600
|
+
function parseProjectFromUri(uri) {
|
|
7601
|
+
const parts = uri.split("/memories/")[1]?.split("/").filter(Boolean) ?? [];
|
|
7602
|
+
if ((parts[0] === "handoffs" || parts[0] === "incidents") && parts[1] === "active") {
|
|
7603
|
+
return parts[2];
|
|
7604
|
+
}
|
|
7605
|
+
if (parts[0] === "durable" && parts[1] === "projects") {
|
|
7606
|
+
return parts[2];
|
|
7607
|
+
}
|
|
7608
|
+
return void 0;
|
|
7609
|
+
}
|
|
7610
|
+
function comparableMemoryBody(body) {
|
|
7611
|
+
return stripHygieneSources(body).trim().replace(/\s+/g, " ");
|
|
7612
|
+
}
|
|
7613
|
+
function stripHygieneSources(body) {
|
|
7614
|
+
const index = body.indexOf(`
|
|
7615
|
+
${HYGIENE_SOURCES_HEADING}`);
|
|
7616
|
+
if (index !== -1) {
|
|
7617
|
+
return body.slice(0, index).trim();
|
|
7618
|
+
}
|
|
7619
|
+
return body.startsWith(HYGIENE_SOURCES_HEADING) ? "" : body.trim();
|
|
7620
|
+
}
|
|
7621
|
+
function preferredKeepRecord(records, topic) {
|
|
7622
|
+
const stableRecords = records.filter((record) => isStableRecord(record, topic));
|
|
7623
|
+
return sortedNewestFirst(stableRecords.length > 0 ? stableRecords : records)[0] ?? records[0];
|
|
7624
|
+
}
|
|
7625
|
+
function isStableRecord(record, topic) {
|
|
7626
|
+
const recordTopic = topic ?? topicForRecord(record);
|
|
7627
|
+
return recordTopic !== void 0 && (0, import_node_path3.basename)(record.uri) === `${uriSegment(recordTopic)}.md`;
|
|
7628
|
+
}
|
|
7629
|
+
function sortedNewestFirst(records) {
|
|
7630
|
+
return [...records].sort((left, right) => {
|
|
7631
|
+
const timestampDiff = timestampMs(right) - timestampMs(left);
|
|
7632
|
+
return timestampDiff === 0 ? left.uri.localeCompare(right.uri) : timestampDiff;
|
|
7633
|
+
});
|
|
7634
|
+
}
|
|
7635
|
+
function timestampMs(record) {
|
|
7636
|
+
const parsed = Date.parse(record.metadata.timestamp);
|
|
7637
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
7638
|
+
}
|
|
7639
|
+
function isStaleLookingHandoff(record, now) {
|
|
7640
|
+
if (record.metadata.kind !== "handoff") {
|
|
7641
|
+
return false;
|
|
7642
|
+
}
|
|
7643
|
+
if (now.getTime() - timestampMs(record) < STALE_HANDOFF_AGE_MS) {
|
|
7644
|
+
return false;
|
|
7645
|
+
}
|
|
7646
|
+
return /\b(?:PR|pull request)\s+(?:OPEN|open|is open)|awaiting review|waiting for review|next steps?:\s*address PR review/i.test(
|
|
7647
|
+
record.body
|
|
7648
|
+
);
|
|
7649
|
+
}
|
|
7650
|
+
function groupBy(values, keyForValue) {
|
|
7651
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7652
|
+
for (const value of values) {
|
|
7653
|
+
const key = keyForValue(value);
|
|
7654
|
+
groups.set(key, [...groups.get(key) ?? [], value]);
|
|
7655
|
+
}
|
|
7656
|
+
return groups;
|
|
7657
|
+
}
|
|
7658
|
+
function compareGroupedRecordLists(left, right) {
|
|
7659
|
+
return (left[0]?.groupKey ?? "").localeCompare(right[0]?.groupKey ?? "");
|
|
7660
|
+
}
|
|
7661
|
+
function dedupeByUri(items) {
|
|
7662
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7663
|
+
const result = [];
|
|
7664
|
+
for (const item of items) {
|
|
7665
|
+
if (seen.has(item.uri)) {
|
|
7666
|
+
continue;
|
|
7667
|
+
}
|
|
7668
|
+
seen.add(item.uri);
|
|
7669
|
+
result.push(item);
|
|
7670
|
+
}
|
|
7671
|
+
return result;
|
|
7672
|
+
}
|
|
7673
|
+
function formatMemoryDocument(title, metadata, body) {
|
|
7674
|
+
const header = [
|
|
7675
|
+
title,
|
|
7676
|
+
`kind: ${metadata.kind}`,
|
|
7677
|
+
`status: ${metadata.status}`,
|
|
7678
|
+
metadata.project ? `project: ${metadata.project}` : void 0,
|
|
7679
|
+
metadata.topic ? `topic: ${metadata.topic}` : void 0,
|
|
7680
|
+
`source_agent_client: ${metadata.sourceAgentClient}`,
|
|
7681
|
+
`timestamp: ${metadata.timestamp}`,
|
|
7682
|
+
metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
|
|
7683
|
+
metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
|
|
7684
|
+
].filter((line) => line !== void 0);
|
|
7685
|
+
return [...header, "", body.trim()].join("\n");
|
|
7686
|
+
}
|
|
7687
|
+
function headerValue(header, key) {
|
|
7688
|
+
const prefix = `${key}:`;
|
|
7689
|
+
return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
|
|
7690
|
+
}
|
|
7691
|
+
function parseOptionalMemoryKind(value) {
|
|
7692
|
+
if (!value) {
|
|
7693
|
+
return void 0;
|
|
7694
|
+
}
|
|
7695
|
+
if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
|
|
7696
|
+
return value;
|
|
7697
|
+
}
|
|
7698
|
+
return void 0;
|
|
7699
|
+
}
|
|
7700
|
+
function parseOptionalMemoryStatus(value) {
|
|
7701
|
+
if (!value) {
|
|
7702
|
+
return void 0;
|
|
7703
|
+
}
|
|
7704
|
+
if (["active", "archived", "superseded"].includes(value)) {
|
|
7705
|
+
return value;
|
|
7706
|
+
}
|
|
7707
|
+
return void 0;
|
|
7708
|
+
}
|
|
7709
|
+
function normalizeOptionalMetadata(value) {
|
|
7710
|
+
const trimmed = value?.trim();
|
|
7711
|
+
return trimmed ? trimmed : void 0;
|
|
7712
|
+
}
|
|
7713
|
+
function isCompactableKind(kind) {
|
|
7714
|
+
return kind === "durable" || kind === "handoff" || kind === "incident";
|
|
7715
|
+
}
|
|
7716
|
+
function memoryKindPlural(kind) {
|
|
7717
|
+
switch (kind) {
|
|
7718
|
+
case "handoff":
|
|
7719
|
+
return "handoffs";
|
|
7720
|
+
case "incident":
|
|
7721
|
+
return "incidents";
|
|
7722
|
+
case "durable":
|
|
7723
|
+
return "durable memories";
|
|
7724
|
+
case "preference":
|
|
7725
|
+
return "preferences";
|
|
7726
|
+
case "smoke":
|
|
7727
|
+
return "smoke memories";
|
|
7728
|
+
}
|
|
7729
|
+
}
|
|
7730
|
+
function formatPlanSection(title, lines) {
|
|
7731
|
+
if (lines.length === 0) {
|
|
7732
|
+
return `${title}:
|
|
7733
|
+
- none`;
|
|
7734
|
+
}
|
|
7735
|
+
return [`${title}:`, ...lines.map((line) => `- ${line}`)].join("\n");
|
|
7736
|
+
}
|
|
7737
|
+
|
|
7221
7738
|
// src/runtime.ts
|
|
7222
7739
|
var import_node_fs3 = require("node:fs");
|
|
7223
7740
|
var import_node_os3 = require("node:os");
|
|
7224
|
-
var
|
|
7741
|
+
var import_node_path4 = require("node:path");
|
|
7225
7742
|
function getRuntimeConfig(program2, manifestOverride) {
|
|
7226
7743
|
const options = program2.opts();
|
|
7227
7744
|
const threadnoteHome = expandPath(options.home ?? process.env.THREADNOTE_HOME ?? "~/.openviking");
|
|
@@ -7240,32 +7757,36 @@ function getRuntimeConfig(program2, manifestOverride) {
|
|
|
7240
7757
|
};
|
|
7241
7758
|
}
|
|
7242
7759
|
function defaultManifestPath(agentContextHome) {
|
|
7243
|
-
const userManifest = (0,
|
|
7760
|
+
const userManifest = (0, import_node_path4.join)(agentContextHome, USER_MANIFEST_NAME);
|
|
7244
7761
|
return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
|
|
7245
7762
|
}
|
|
7246
7763
|
function builtInExampleManifestPath() {
|
|
7247
|
-
return (0,
|
|
7764
|
+
return (0, import_node_path4.join)(toolRoot(), "config", "seed-manifest.example.yaml");
|
|
7248
7765
|
}
|
|
7249
7766
|
function openVikingHealthUrl(config) {
|
|
7250
7767
|
return `http://${config.host}:${config.port}/health`;
|
|
7251
7768
|
}
|
|
7252
7769
|
function openVikingLogPath(config) {
|
|
7253
|
-
return (0,
|
|
7770
|
+
return (0, import_node_path4.join)(config.agentContextHome, "logs", "server.log");
|
|
7254
7771
|
}
|
|
7255
7772
|
function openVikingServerArgs(config) {
|
|
7256
|
-
return ["--config", (0,
|
|
7773
|
+
return ["--config", (0, import_node_path4.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
|
|
7257
7774
|
}
|
|
7258
7775
|
function withIdentity(config, args) {
|
|
7259
7776
|
return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
|
|
7260
7777
|
}
|
|
7261
|
-
function renderTemplate(template, config) {
|
|
7262
|
-
|
|
7778
|
+
function renderTemplate(template, config, extras = {}) {
|
|
7779
|
+
let rendered = 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);
|
|
7780
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
7781
|
+
rendered = rendered.replaceAll(`{{${key}}}`, () => value);
|
|
7782
|
+
}
|
|
7783
|
+
return rendered;
|
|
7263
7784
|
}
|
|
7264
7785
|
|
|
7265
7786
|
// src/share.ts
|
|
7266
7787
|
var import_promises4 = require("node:fs/promises");
|
|
7267
7788
|
var import_node_os4 = require("node:os");
|
|
7268
|
-
var
|
|
7789
|
+
var import_node_path5 = require("node:path");
|
|
7269
7790
|
var TEAMS_FILE_VERSION = 1;
|
|
7270
7791
|
var SHARED_SEGMENT = "shared";
|
|
7271
7792
|
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
@@ -7334,8 +7855,8 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
7334
7855
|
if (await exists(gitdir)) {
|
|
7335
7856
|
throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
|
|
7336
7857
|
}
|
|
7337
|
-
await ensureDirectory((0,
|
|
7338
|
-
await ensureDirectory((0,
|
|
7858
|
+
await ensureDirectory((0, import_node_path5.dirname)(worktree), dryRun);
|
|
7859
|
+
await ensureDirectory((0, import_node_path5.dirname)(gitdir), dryRun);
|
|
7339
7860
|
const git = await requiredExecutable("git");
|
|
7340
7861
|
await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
|
|
7341
7862
|
const newConfig = {
|
|
@@ -7366,7 +7887,7 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
7366
7887
|
var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
|
|
7367
7888
|
var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
|
|
7368
7889
|
async function ensureSharedGitignore(worktree, git, push) {
|
|
7369
|
-
const gitignorePath = (0,
|
|
7890
|
+
const gitignorePath = (0, import_node_path5.join)(worktree, ".gitignore");
|
|
7370
7891
|
const existing = await readFileIfExists(gitignorePath) ?? "";
|
|
7371
7892
|
const lines = existing.split("\n").map((line) => line.trim());
|
|
7372
7893
|
const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
|
|
@@ -7467,7 +7988,7 @@ function autoShareState(config) {
|
|
|
7467
7988
|
return state;
|
|
7468
7989
|
}
|
|
7469
7990
|
function pendingReindexesPath(config) {
|
|
7470
|
-
return (0,
|
|
7991
|
+
return (0, import_node_path5.join)(config.agentContextHome, "share", "auto-sync-pending-reindexes.json");
|
|
7471
7992
|
}
|
|
7472
7993
|
async function loadPendingReindexes(config, state) {
|
|
7473
7994
|
const raw = await readFileIfExists(pendingReindexesPath(config));
|
|
@@ -7511,7 +8032,7 @@ async function writePendingReindexes(config, state) {
|
|
|
7511
8032
|
teams: Object.fromEntries(state.pendingReindexes),
|
|
7512
8033
|
version: 1
|
|
7513
8034
|
};
|
|
7514
|
-
await (0, import_promises4.mkdir)((0,
|
|
8035
|
+
await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
|
|
7515
8036
|
const tempPath = `${path}.${process.pid}.tmp`;
|
|
7516
8037
|
await (0, import_promises4.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
|
|
7517
8038
|
`, { encoding: "utf8", mode: 384 });
|
|
@@ -7604,7 +8125,7 @@ async function runShareSync(config, options) {
|
|
|
7604
8125
|
if (dryRun) {
|
|
7605
8126
|
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
7606
8127
|
} else if (pullResult && pullResult.exitCode !== 0) {
|
|
7607
|
-
if (await exists((0,
|
|
8128
|
+
if (await exists((0, import_node_path5.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path5.join)(team.config.gitdir, "rebase-apply"))) {
|
|
7608
8129
|
throw new Error(
|
|
7609
8130
|
`git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
|
|
7610
8131
|
Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
|
|
@@ -7659,7 +8180,7 @@ async function runShareSyncQuiet(config, state, options) {
|
|
|
7659
8180
|
const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
|
|
7660
8181
|
const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
|
|
7661
8182
|
if (pullResult.exitCode !== 0) {
|
|
7662
|
-
if (await exists((0,
|
|
8183
|
+
if (await exists((0, import_node_path5.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path5.join)(team.config.gitdir, "rebase-apply"))) {
|
|
7663
8184
|
throw new Error(
|
|
7664
8185
|
`Automatic share sync hit git conflicts in ${worktree}. Resolve them in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then rerun recall/read.`
|
|
7665
8186
|
);
|
|
@@ -7833,10 +8354,10 @@ function normalizeTeamName(input2) {
|
|
|
7833
8354
|
return candidate;
|
|
7834
8355
|
}
|
|
7835
8356
|
function teamsFilePath(config) {
|
|
7836
|
-
return (0,
|
|
8357
|
+
return (0, import_node_path5.join)(config.agentContextHome, "share", "teams.json");
|
|
7837
8358
|
}
|
|
7838
8359
|
function teamWorktreePath(config, team) {
|
|
7839
|
-
return (0,
|
|
8360
|
+
return (0, import_node_path5.join)(
|
|
7840
8361
|
config.agentContextHome,
|
|
7841
8362
|
"data",
|
|
7842
8363
|
"viking",
|
|
@@ -7849,7 +8370,7 @@ function teamWorktreePath(config, team) {
|
|
|
7849
8370
|
);
|
|
7850
8371
|
}
|
|
7851
8372
|
function teamGitdirPath(config, team) {
|
|
7852
|
-
return (0,
|
|
8373
|
+
return (0, import_node_path5.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
|
|
7853
8374
|
}
|
|
7854
8375
|
async function readTeamsFile(config) {
|
|
7855
8376
|
const path = teamsFilePath(config);
|
|
@@ -7892,7 +8413,7 @@ async function readTeamsFile(config) {
|
|
|
7892
8413
|
}
|
|
7893
8414
|
async function writeTeamsFile(config, contents) {
|
|
7894
8415
|
const path = teamsFilePath(config);
|
|
7895
|
-
await (0, import_promises4.mkdir)((0,
|
|
8416
|
+
await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
|
|
7896
8417
|
const serializable = {
|
|
7897
8418
|
defaultTeam: contents.defaultTeam,
|
|
7898
8419
|
teams: contents.teams,
|
|
@@ -7961,7 +8482,7 @@ async function walkMemoryFiles(root) {
|
|
|
7961
8482
|
if (entry.name === ".git") {
|
|
7962
8483
|
continue;
|
|
7963
8484
|
}
|
|
7964
|
-
const full = (0,
|
|
8485
|
+
const full = (0, import_node_path5.join)(path, entry.name);
|
|
7965
8486
|
if (entry.isDirectory()) {
|
|
7966
8487
|
if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
|
|
7967
8488
|
continue;
|
|
@@ -7985,7 +8506,7 @@ async function walkMemoryFiles(root) {
|
|
|
7985
8506
|
return out;
|
|
7986
8507
|
}
|
|
7987
8508
|
function workfileToVikingUri(config, team, filePath) {
|
|
7988
|
-
const rel = (0,
|
|
8509
|
+
const rel = (0, import_node_path5.relative)(team.worktree, filePath).split(import_node_path5.sep).join("/");
|
|
7989
8510
|
return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
7990
8511
|
}
|
|
7991
8512
|
function isInSharedNamespace(config, uri) {
|
|
@@ -8105,8 +8626,8 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun, op
|
|
|
8105
8626
|
}
|
|
8106
8627
|
return;
|
|
8107
8628
|
}
|
|
8108
|
-
const stagingDir = await (0, import_promises4.mkdtemp)((0,
|
|
8109
|
-
const tempPath = (0,
|
|
8629
|
+
const stagingDir = await (0, import_promises4.mkdtemp)((0, import_node_path5.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
|
|
8630
|
+
const tempPath = (0, import_node_path5.join)(stagingDir, "body.txt");
|
|
8110
8631
|
try {
|
|
8111
8632
|
await (0, import_promises4.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
|
|
8112
8633
|
await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode, options);
|
|
@@ -8228,7 +8749,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
|
8228
8749
|
if (!relative3) {
|
|
8229
8750
|
return;
|
|
8230
8751
|
}
|
|
8231
|
-
await (0, import_promises4.rm)((0,
|
|
8752
|
+
await (0, import_promises4.rm)((0, import_node_path5.join)(worktree, relative3), { force: true });
|
|
8232
8753
|
}
|
|
8233
8754
|
async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
|
|
8234
8755
|
const args = withIdentity(config, ["rm", uri]);
|
|
@@ -8292,8 +8813,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
8292
8813
|
const oldRel = entries[index + 1];
|
|
8293
8814
|
const newRel = entries[index + 2];
|
|
8294
8815
|
if (oldRel && newRel) {
|
|
8295
|
-
changes.push({ path: (0,
|
|
8296
|
-
changes.push({ path: (0,
|
|
8816
|
+
changes.push({ path: (0, import_node_path5.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
|
|
8817
|
+
changes.push({ path: (0, import_node_path5.join)(worktree, newRel), relativePath: newRel, status: "added" });
|
|
8297
8818
|
}
|
|
8298
8819
|
index += 3;
|
|
8299
8820
|
continue;
|
|
@@ -8301,7 +8822,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
8301
8822
|
const rel = entries[index + 1];
|
|
8302
8823
|
if (rel) {
|
|
8303
8824
|
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
8304
|
-
changes.push({ path: (0,
|
|
8825
|
+
changes.push({ path: (0, import_node_path5.join)(worktree, rel), relativePath: rel, status });
|
|
8305
8826
|
}
|
|
8306
8827
|
index += 2;
|
|
8307
8828
|
}
|
|
@@ -8385,6 +8906,12 @@ function parseMemoryStatus(value) {
|
|
|
8385
8906
|
}
|
|
8386
8907
|
throw new Error(`Unsupported memory status "${value}". Expected active, archived, or superseded.`);
|
|
8387
8908
|
}
|
|
8909
|
+
function parseCompactKind(value) {
|
|
8910
|
+
if (["durable", "handoff", "incident"].includes(value)) {
|
|
8911
|
+
return value;
|
|
8912
|
+
}
|
|
8913
|
+
throw new Error(`Unsupported compact kind "${value}". Expected durable, handoff, or incident.`);
|
|
8914
|
+
}
|
|
8388
8915
|
async function runRemember(config, options) {
|
|
8389
8916
|
const text = await getInputText(options.text, options.stdin === true);
|
|
8390
8917
|
if (!text.trim()) {
|
|
@@ -8392,11 +8919,11 @@ async function runRemember(config, options) {
|
|
|
8392
8919
|
}
|
|
8393
8920
|
const metadata = {
|
|
8394
8921
|
kind: options.kind ?? "durable",
|
|
8395
|
-
project:
|
|
8922
|
+
project: normalizeOptionalMetadata2(options.project),
|
|
8396
8923
|
sourceAgentClient: options.sourceAgentClient ?? "codex",
|
|
8397
8924
|
status: options.status ?? "active",
|
|
8398
8925
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8399
|
-
topic:
|
|
8926
|
+
topic: normalizeOptionalMetadata2(options.topic)
|
|
8400
8927
|
};
|
|
8401
8928
|
await storeMemory(config, {
|
|
8402
8929
|
bodyText: text.trim(),
|
|
@@ -8417,7 +8944,7 @@ async function runMigrateMemories(config, options) {
|
|
|
8417
8944
|
const candidates = await legacyMemoryCandidates(config, sourceAccounts);
|
|
8418
8945
|
const existingHashes = await existingDurableMemoryHashes(config);
|
|
8419
8946
|
const ov = await openVikingCliForMode(dryRun);
|
|
8420
|
-
const migrationPath = (0,
|
|
8947
|
+
const migrationPath = (0, import_node_path6.join)(config.agentContextHome, "legacy-memory-migration.txt");
|
|
8421
8948
|
let duplicateCount = 0;
|
|
8422
8949
|
let migratedCount = 0;
|
|
8423
8950
|
let sensitiveCount = 0;
|
|
@@ -8480,7 +9007,7 @@ async function runMigrateLifecycle(config, options) {
|
|
|
8480
9007
|
const limit = options.limit ? parsePositiveInteger(options.limit, "lifecycle migration limit") : void 0;
|
|
8481
9008
|
const ov = await openVikingCliForMode(dryRun);
|
|
8482
9009
|
const candidates = await legacyLifecycleHandoffCandidates(config);
|
|
8483
|
-
const migrationPath = (0,
|
|
9010
|
+
const migrationPath = (0, import_node_path6.join)(config.agentContextHome, "lifecycle-memory-migration.txt");
|
|
8484
9011
|
let existingCount = 0;
|
|
8485
9012
|
let migratedCount = 0;
|
|
8486
9013
|
let skippedCount = 0;
|
|
@@ -8490,7 +9017,7 @@ async function runMigrateLifecycle(config, options) {
|
|
|
8490
9017
|
break;
|
|
8491
9018
|
}
|
|
8492
9019
|
const destinationUri = lifecycleMigrationUri(config, candidate.metadata, sha256(candidate.original.trim()));
|
|
8493
|
-
const migratedMemory =
|
|
9020
|
+
const migratedMemory = formatMemoryDocument2(
|
|
8494
9021
|
"HANDOFF",
|
|
8495
9022
|
candidate.metadata,
|
|
8496
9023
|
["Migrated legacy handoff from the historical events trail.", "", candidate.original.trim()].join("\n")
|
|
@@ -8536,8 +9063,10 @@ async function runRecall(config, options) {
|
|
|
8536
9063
|
await syncSharedReposAndLog(config);
|
|
8537
9064
|
}
|
|
8538
9065
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8539
|
-
const
|
|
8540
|
-
const
|
|
9066
|
+
const query = await enrichRecallQueryWithWorkspaceContext(options.query);
|
|
9067
|
+
const projectQuery = await enrichRecallQueryWithWorkspaceProjectContext(options.query);
|
|
9068
|
+
const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, projectQuery));
|
|
9069
|
+
const args = ["search", query];
|
|
8541
9070
|
if (inferredUri) {
|
|
8542
9071
|
args.push("--uri", inferredUri);
|
|
8543
9072
|
console.log(`Recall scope: ${inferredUri}`);
|
|
@@ -8545,24 +9074,41 @@ async function runRecall(config, options) {
|
|
|
8545
9074
|
if (options.nodeLimit) {
|
|
8546
9075
|
args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
|
|
8547
9076
|
}
|
|
8548
|
-
|
|
8549
|
-
await
|
|
8550
|
-
|
|
9077
|
+
const recallOutputs = [];
|
|
9078
|
+
const baseResult = await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
9079
|
+
if (baseResult) {
|
|
9080
|
+
recallOutputs.push([baseResult.stdout.trim(), baseResult.stderr.trim()].filter(Boolean).join("\n"));
|
|
9081
|
+
}
|
|
9082
|
+
const seededOutput = await augmentRecallWithSeededResources(
|
|
9083
|
+
config,
|
|
9084
|
+
ov,
|
|
9085
|
+
{ ...options, query },
|
|
9086
|
+
inferredUri,
|
|
9087
|
+
projectQuery
|
|
9088
|
+
);
|
|
9089
|
+
if (seededOutput) {
|
|
9090
|
+
recallOutputs.push(seededOutput);
|
|
9091
|
+
}
|
|
9092
|
+
const exactOutput = await printExactMemoryMatches(config, ov, query, {
|
|
8551
9093
|
dryRun: options.dryRun === true,
|
|
8552
9094
|
includeArchived: options.includeArchived === true
|
|
8553
9095
|
});
|
|
9096
|
+
if (exactOutput) {
|
|
9097
|
+
recallOutputs.push(exactOutput);
|
|
9098
|
+
}
|
|
9099
|
+
await printRecallHygieneNudges(config, recallOutputs.join("\n"));
|
|
8554
9100
|
}
|
|
8555
|
-
async function augmentRecallWithSeededResources(config, ov, options, inferredUri) {
|
|
9101
|
+
async function augmentRecallWithSeededResources(config, ov, options, inferredUri, projectQuery = options.query) {
|
|
8556
9102
|
if (options.uri || options.inferScope === false) {
|
|
8557
|
-
return;
|
|
9103
|
+
return void 0;
|
|
8558
9104
|
}
|
|
8559
|
-
const project = await inferProjectFromQuery(config.manifestPath,
|
|
9105
|
+
const project = await inferProjectFromQuery(config.manifestPath, projectQuery);
|
|
8560
9106
|
if (!project) {
|
|
8561
|
-
return;
|
|
9107
|
+
return void 0;
|
|
8562
9108
|
}
|
|
8563
9109
|
const projectResourceUri = trimTrailingSlash(project.uri);
|
|
8564
9110
|
if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
|
|
8565
|
-
return;
|
|
9111
|
+
return void 0;
|
|
8566
9112
|
}
|
|
8567
9113
|
const args = ["search", options.query, "--uri", projectResourceUri];
|
|
8568
9114
|
if (options.nodeLimit) {
|
|
@@ -8570,7 +9116,8 @@ async function augmentRecallWithSeededResources(config, ov, options, inferredUri
|
|
|
8570
9116
|
}
|
|
8571
9117
|
console.log(`
|
|
8572
9118
|
Also searching seeded resources: ${projectResourceUri}`);
|
|
8573
|
-
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
9119
|
+
const result = await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
9120
|
+
return result ? [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n") : void 0;
|
|
8574
9121
|
}
|
|
8575
9122
|
async function runRead(config, uri, options) {
|
|
8576
9123
|
assertVikingUri(uri);
|
|
@@ -8598,6 +9145,147 @@ async function syncSharedReposAndLog(config) {
|
|
|
8598
9145
|
console.error(`Auto-sync warning: ${err instanceof Error ? err.message : String(err)}`);
|
|
8599
9146
|
}
|
|
8600
9147
|
}
|
|
9148
|
+
async function printRecallHygieneNudges(config, recallOutput) {
|
|
9149
|
+
const uris = activePersonalMemoryUrisFromText(recallOutput, config.user);
|
|
9150
|
+
if (uris.length === 0) {
|
|
9151
|
+
return;
|
|
9152
|
+
}
|
|
9153
|
+
const records = await readMemoryRecordsByUri(config, uris);
|
|
9154
|
+
const nudges = recallHygieneNudges(recallOutput, { records, user: config.user });
|
|
9155
|
+
if (nudges.length === 0) {
|
|
9156
|
+
return;
|
|
9157
|
+
}
|
|
9158
|
+
console.log("\nMemory hygiene hints:");
|
|
9159
|
+
for (const nudge of nudges) {
|
|
9160
|
+
console.log(`- ${nudge}`);
|
|
9161
|
+
}
|
|
9162
|
+
}
|
|
9163
|
+
async function runCompact(config, options) {
|
|
9164
|
+
const project = normalizeOptionalMetadata2(options.project);
|
|
9165
|
+
if (!project) {
|
|
9166
|
+
throw new Error("Provide --project for scoped memory hygiene.");
|
|
9167
|
+
}
|
|
9168
|
+
if (options.apply === true && options.dryRun === true) {
|
|
9169
|
+
throw new Error("Cannot combine --apply with --dry-run.");
|
|
9170
|
+
}
|
|
9171
|
+
const apply = options.apply === true;
|
|
9172
|
+
const records = await scopedCompactRecords(config, {
|
|
9173
|
+
kind: options.kind,
|
|
9174
|
+
project
|
|
9175
|
+
});
|
|
9176
|
+
const plan = buildCompactPlan(records, {
|
|
9177
|
+
kind: options.kind,
|
|
9178
|
+
project,
|
|
9179
|
+
topic: normalizeOptionalMetadata2(options.topic)
|
|
9180
|
+
});
|
|
9181
|
+
console.log(formatCompactPlan(plan, { apply }));
|
|
9182
|
+
if (!apply) {
|
|
9183
|
+
return;
|
|
9184
|
+
}
|
|
9185
|
+
const ov = await openVikingCliForMode(false);
|
|
9186
|
+
const updatePath = (0, import_node_path6.join)(config.agentContextHome, "compact-memory-update.txt");
|
|
9187
|
+
try {
|
|
9188
|
+
for (const action of plan.keepUpdates) {
|
|
9189
|
+
await (0, import_promises5.writeFile)(updatePath, action.content, { encoding: "utf8", mode: 384 });
|
|
9190
|
+
await (0, import_promises5.chmod)(updatePath, 384);
|
|
9191
|
+
await writeDurableMemoryFile(ov, config, action.uri, updatePath, "replace");
|
|
9192
|
+
}
|
|
9193
|
+
} finally {
|
|
9194
|
+
await (0, import_promises5.rm)(updatePath, { force: true });
|
|
9195
|
+
}
|
|
9196
|
+
for (const action of plan.archives) {
|
|
9197
|
+
await runArchive(config, action.uri, {
|
|
9198
|
+
dryRun: false,
|
|
9199
|
+
kind: action.kind,
|
|
9200
|
+
project: action.project,
|
|
9201
|
+
topic: action.topic
|
|
9202
|
+
});
|
|
9203
|
+
}
|
|
9204
|
+
for (const action of plan.forgets) {
|
|
9205
|
+
await runForget(config, action.uri, { dryRun: false });
|
|
9206
|
+
}
|
|
9207
|
+
}
|
|
9208
|
+
async function scopedCompactRecords(config, options) {
|
|
9209
|
+
const kinds = options.kind ? [options.kind] : ["handoff", "durable", "incident"];
|
|
9210
|
+
const records = [];
|
|
9211
|
+
for (const kind of kinds) {
|
|
9212
|
+
const directory = localMemoryDirectoryForCompact(config, kind, options.project);
|
|
9213
|
+
const uriDirectory = memoryUriDirectoryForCompact(config, kind, options.project);
|
|
9214
|
+
let entries;
|
|
9215
|
+
try {
|
|
9216
|
+
entries = await (0, import_promises5.readdir)(directory, { withFileTypes: true });
|
|
9217
|
+
} catch (_err) {
|
|
9218
|
+
continue;
|
|
9219
|
+
}
|
|
9220
|
+
for (const entry of entries) {
|
|
9221
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
|
|
9222
|
+
continue;
|
|
9223
|
+
}
|
|
9224
|
+
const content = await readTextIfExists((0, import_node_path6.join)(directory, entry.name));
|
|
9225
|
+
if (!content) {
|
|
9226
|
+
continue;
|
|
9227
|
+
}
|
|
9228
|
+
const record = parseMemoryDocument(`${uriDirectory}/${entry.name}`, content);
|
|
9229
|
+
if (record) {
|
|
9230
|
+
records.push(record);
|
|
9231
|
+
}
|
|
9232
|
+
}
|
|
9233
|
+
}
|
|
9234
|
+
return records;
|
|
9235
|
+
}
|
|
9236
|
+
async function readMemoryRecordsByUri(config, uris) {
|
|
9237
|
+
const records = [];
|
|
9238
|
+
for (const uri of uris) {
|
|
9239
|
+
const localPath = localMemoryPathForUri(config, uri);
|
|
9240
|
+
if (!localPath) {
|
|
9241
|
+
continue;
|
|
9242
|
+
}
|
|
9243
|
+
const content = await readTextIfExists(localPath);
|
|
9244
|
+
if (!content) {
|
|
9245
|
+
continue;
|
|
9246
|
+
}
|
|
9247
|
+
const record = parseMemoryDocument(uri, content);
|
|
9248
|
+
if (record) {
|
|
9249
|
+
records.push(record);
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
9252
|
+
return records;
|
|
9253
|
+
}
|
|
9254
|
+
function localMemoryDirectoryForCompact(config, kind, project) {
|
|
9255
|
+
const root = localUserMemoriesRoot(config);
|
|
9256
|
+
const projectSegment = uriSegment(project);
|
|
9257
|
+
switch (kind) {
|
|
9258
|
+
case "durable":
|
|
9259
|
+
return (0, import_node_path6.join)(root, "durable", "projects", projectSegment);
|
|
9260
|
+
case "handoff":
|
|
9261
|
+
return (0, import_node_path6.join)(root, "handoffs", "active", projectSegment);
|
|
9262
|
+
case "incident":
|
|
9263
|
+
return (0, import_node_path6.join)(root, "incidents", "active", projectSegment);
|
|
9264
|
+
}
|
|
9265
|
+
}
|
|
9266
|
+
function memoryUriDirectoryForCompact(config, kind, project) {
|
|
9267
|
+
const base = `viking://user/${uriSegment(config.user)}/memories`;
|
|
9268
|
+
const projectSegment = uriSegment(project);
|
|
9269
|
+
switch (kind) {
|
|
9270
|
+
case "durable":
|
|
9271
|
+
return `${base}/durable/projects/${projectSegment}`;
|
|
9272
|
+
case "handoff":
|
|
9273
|
+
return `${base}/handoffs/active/${projectSegment}`;
|
|
9274
|
+
case "incident":
|
|
9275
|
+
return `${base}/incidents/active/${projectSegment}`;
|
|
9276
|
+
}
|
|
9277
|
+
}
|
|
9278
|
+
function localMemoryPathForUri(config, uri) {
|
|
9279
|
+
const prefix = `viking://user/${uriSegment(config.user)}/memories/`;
|
|
9280
|
+
if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
|
|
9281
|
+
return void 0;
|
|
9282
|
+
}
|
|
9283
|
+
const relative3 = uri.slice(prefix.length);
|
|
9284
|
+
if (relative3.includes("..") || relative3.startsWith("/")) {
|
|
9285
|
+
return void 0;
|
|
9286
|
+
}
|
|
9287
|
+
return (0, import_node_path6.join)(localUserMemoriesRoot(config), ...relative3.split("/"));
|
|
9288
|
+
}
|
|
8601
9289
|
async function runList(config, uri, options) {
|
|
8602
9290
|
assertVikingUri(uri);
|
|
8603
9291
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
@@ -8635,11 +9323,11 @@ async function runArchive(config, uri, options) {
|
|
|
8635
9323
|
const fallbackMetadata = {
|
|
8636
9324
|
archivedFrom: uri,
|
|
8637
9325
|
kind: options.kind ?? "handoff",
|
|
8638
|
-
project:
|
|
9326
|
+
project: normalizeOptionalMetadata2(options.project),
|
|
8639
9327
|
sourceAgentClient: "threadnote",
|
|
8640
9328
|
status: "archived",
|
|
8641
9329
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8642
|
-
topic:
|
|
9330
|
+
topic: normalizeOptionalMetadata2(options.topic)
|
|
8643
9331
|
};
|
|
8644
9332
|
await storeMemory(config, {
|
|
8645
9333
|
bodyText: ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n"),
|
|
@@ -8657,11 +9345,11 @@ async function runArchive(config, uri, options) {
|
|
|
8657
9345
|
const metadata = {
|
|
8658
9346
|
archivedFrom: uri,
|
|
8659
9347
|
kind: options.kind ?? inferredMetadata.kind ?? "handoff",
|
|
8660
|
-
project:
|
|
9348
|
+
project: normalizeOptionalMetadata2(options.project) ?? inferredMetadata.project,
|
|
8661
9349
|
sourceAgentClient: "threadnote",
|
|
8662
9350
|
status: "archived",
|
|
8663
9351
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8664
|
-
topic:
|
|
9352
|
+
topic: normalizeOptionalMetadata2(options.topic) ?? inferredMetadata.topic
|
|
8665
9353
|
};
|
|
8666
9354
|
await storeMemory(config, {
|
|
8667
9355
|
bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
|
|
@@ -8690,7 +9378,7 @@ async function runForget(config, uri, options) {
|
|
|
8690
9378
|
}
|
|
8691
9379
|
async function runExportPack(config, options) {
|
|
8692
9380
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8693
|
-
const defaultPath = (0,
|
|
9381
|
+
const defaultPath = (0, import_node_path6.join)(config.agentContextHome, `threadnote-${safeTimestamp()}.ovpack`);
|
|
8694
9382
|
const outputPath = expandPath(options.path ?? defaultPath);
|
|
8695
9383
|
await maybeRun(options.dryRun === true, ov, withIdentity(config, ["export", options.uri ?? "viking://", outputPath]));
|
|
8696
9384
|
}
|
|
@@ -8715,7 +9403,7 @@ async function inferRecallUri(config, query) {
|
|
|
8715
9403
|
async function printExactMemoryMatches(config, ov, query, options) {
|
|
8716
9404
|
const terms = exactRecallTerms(query);
|
|
8717
9405
|
if (terms.length === 0) {
|
|
8718
|
-
return;
|
|
9406
|
+
return void 0;
|
|
8719
9407
|
}
|
|
8720
9408
|
const scopes = exactMemoryScopes(config, options.includeArchived);
|
|
8721
9409
|
const outputs = [];
|
|
@@ -8727,30 +9415,32 @@ async function printExactMemoryMatches(config, ov, query, options) {
|
|
|
8727
9415
|
continue;
|
|
8728
9416
|
}
|
|
8729
9417
|
const result = await runCommand(ov, args, { allowFailure: true });
|
|
8730
|
-
const
|
|
8731
|
-
if (result.exitCode === 0 && grepOutputHasMatches(
|
|
8732
|
-
outputs.push(
|
|
9418
|
+
const output3 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
9419
|
+
if (result.exitCode === 0 && grepOutputHasMatches(output3)) {
|
|
9420
|
+
outputs.push(output3);
|
|
8733
9421
|
}
|
|
8734
9422
|
}
|
|
8735
9423
|
}
|
|
8736
9424
|
if (outputs.length === 0) {
|
|
8737
|
-
return;
|
|
9425
|
+
return void 0;
|
|
8738
9426
|
}
|
|
8739
|
-
console.log("\nExact
|
|
8740
|
-
|
|
9427
|
+
console.log("\nExact memory/resource matches:");
|
|
9428
|
+
const output2 = outputs.join("\n\n");
|
|
9429
|
+
console.log(output2);
|
|
9430
|
+
return output2;
|
|
8741
9431
|
}
|
|
8742
9432
|
async function storeMemory(config, options) {
|
|
8743
9433
|
if (options.replaceUri) {
|
|
8744
9434
|
assertVikingUri(options.replaceUri);
|
|
8745
9435
|
}
|
|
8746
9436
|
const ov = await openVikingCliForMode(options.dryRun);
|
|
8747
|
-
const memoryPath = (0,
|
|
9437
|
+
const memoryPath = (0, import_node_path6.join)(config.agentContextHome, "last-memory.txt");
|
|
8748
9438
|
const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
|
|
8749
|
-
const candidateMemory =
|
|
9439
|
+
const candidateMemory = formatMemoryDocument2(options.title, candidateMetadata, options.bodyText);
|
|
8750
9440
|
const memoryUri = memoryUriFor(config, candidateMemory, candidateMetadata);
|
|
8751
9441
|
const isInPlaceUpdate = options.replaceUri !== void 0 && options.replaceUri === memoryUri;
|
|
8752
9442
|
const finalMetadata = isInPlaceUpdate ? { ...options.metadata, supersedes: void 0 } : candidateMetadata;
|
|
8753
|
-
const memory = isInPlaceUpdate ?
|
|
9443
|
+
const memory = isInPlaceUpdate ? formatMemoryDocument2(options.title, finalMetadata, options.bodyText) : candidateMemory;
|
|
8754
9444
|
const writeMode = await memoryWriteMode(ov, config, memoryUri, finalMetadata);
|
|
8755
9445
|
if (options.dryRun) {
|
|
8756
9446
|
console.log(memory);
|
|
@@ -8892,7 +9582,7 @@ async function hasLegacyLifecycleHandoffCandidates(config) {
|
|
|
8892
9582
|
return (await legacyLifecycleHandoffCandidates(config, 1)).length > 0;
|
|
8893
9583
|
}
|
|
8894
9584
|
async function legacyLifecycleHandoffCandidates(config, limit) {
|
|
8895
|
-
const eventsRoot = (0,
|
|
9585
|
+
const eventsRoot = (0, import_node_path6.join)(localUserMemoriesRoot(config), "events");
|
|
8896
9586
|
let entries;
|
|
8897
9587
|
try {
|
|
8898
9588
|
entries = await (0, import_promises5.readdir)(eventsRoot, { withFileTypes: true });
|
|
@@ -8904,7 +9594,7 @@ async function legacyLifecycleHandoffCandidates(config, limit) {
|
|
|
8904
9594
|
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
|
|
8905
9595
|
continue;
|
|
8906
9596
|
}
|
|
8907
|
-
const sourcePath = (0,
|
|
9597
|
+
const sourcePath = (0, import_node_path6.join)(eventsRoot, entry.name);
|
|
8908
9598
|
const original = await readTextIfExists(sourcePath);
|
|
8909
9599
|
if (!original || !isClearLegacyHandoffMemory(original) || sensitiveMemoryReason(original)) {
|
|
8910
9600
|
continue;
|
|
@@ -8990,7 +9680,7 @@ function vikingDirectoryChain(directoryUri) {
|
|
|
8990
9680
|
}
|
|
8991
9681
|
return chain;
|
|
8992
9682
|
}
|
|
8993
|
-
function
|
|
9683
|
+
function formatMemoryDocument2(title, metadata, body) {
|
|
8994
9684
|
const header = [
|
|
8995
9685
|
title,
|
|
8996
9686
|
`kind: ${metadata.kind}`,
|
|
@@ -9007,10 +9697,10 @@ function formatMemoryDocument(title, metadata, body) {
|
|
|
9007
9697
|
function inferMemoryMetadata(memory) {
|
|
9008
9698
|
const header = memory.slice(0, Math.max(0, memory.indexOf("\n\n")) || memory.length);
|
|
9009
9699
|
const firstLine2 = header.split("\n")[0]?.trim();
|
|
9010
|
-
const kind =
|
|
9011
|
-
const status =
|
|
9012
|
-
const project =
|
|
9013
|
-
const topic =
|
|
9700
|
+
const kind = parseOptionalMemoryKind2(parseHeaderValue(header, "kind")) ?? (firstLine2 === "HANDOFF" ? "handoff" : void 0);
|
|
9701
|
+
const status = parseOptionalMemoryStatus2(parseHeaderValue(header, "status"));
|
|
9702
|
+
const project = normalizeOptionalMetadata2(parseHeaderValue(header, "project")) ?? normalizeOptionalMetadata2(parseHeaderValue(header, "repo"));
|
|
9703
|
+
const topic = normalizeOptionalMetadata2(parseHeaderValue(header, "topic")) ?? normalizeOptionalMetadata2(parseHeaderValue(header, "task"));
|
|
9014
9704
|
return {
|
|
9015
9705
|
kind,
|
|
9016
9706
|
project,
|
|
@@ -9046,9 +9736,9 @@ function inferLegacyProject(memory) {
|
|
|
9046
9736
|
return "general";
|
|
9047
9737
|
}
|
|
9048
9738
|
const trimmed = explicit.trim().replace(/[`.,;]+$/g, "");
|
|
9049
|
-
return trimmed.includes("/") ? (0,
|
|
9739
|
+
return trimmed.includes("/") ? (0, import_node_path6.basename)(trimmed) : trimmed;
|
|
9050
9740
|
}
|
|
9051
|
-
function
|
|
9741
|
+
function parseOptionalMemoryKind2(value) {
|
|
9052
9742
|
if (!value) {
|
|
9053
9743
|
return void 0;
|
|
9054
9744
|
}
|
|
@@ -9058,7 +9748,7 @@ function parseOptionalMemoryKind(value) {
|
|
|
9058
9748
|
return void 0;
|
|
9059
9749
|
}
|
|
9060
9750
|
}
|
|
9061
|
-
function
|
|
9751
|
+
function parseOptionalMemoryStatus2(value) {
|
|
9062
9752
|
if (!value) {
|
|
9063
9753
|
return void 0;
|
|
9064
9754
|
}
|
|
@@ -9068,7 +9758,7 @@ function parseOptionalMemoryStatus(value) {
|
|
|
9068
9758
|
return void 0;
|
|
9069
9759
|
}
|
|
9070
9760
|
}
|
|
9071
|
-
function
|
|
9761
|
+
function normalizeOptionalMetadata2(value) {
|
|
9072
9762
|
const trimmed = value?.trim();
|
|
9073
9763
|
return trimmed ? trimmed : void 0;
|
|
9074
9764
|
}
|
|
@@ -9086,14 +9776,14 @@ async function legacySourceAccounts(config, options) {
|
|
|
9086
9776
|
async function legacyMemoryCandidates(config, sourceAccounts) {
|
|
9087
9777
|
const candidates = [];
|
|
9088
9778
|
for (const sourceAccount of sourceAccounts) {
|
|
9089
|
-
const sessionRoot = (0,
|
|
9779
|
+
const sessionRoot = (0, import_node_path6.join)(localVikingDataRoot(config), sourceAccount, "session");
|
|
9090
9780
|
for (const sourceSession of await childDirectoryNames(sessionRoot)) {
|
|
9091
|
-
const historyRoot = (0,
|
|
9781
|
+
const historyRoot = (0, import_node_path6.join)(sessionRoot, sourceSession, "history");
|
|
9092
9782
|
for (const sourceArchive of await childDirectoryNames(historyRoot)) {
|
|
9093
9783
|
if (!sourceArchive.startsWith("archive_")) {
|
|
9094
9784
|
continue;
|
|
9095
9785
|
}
|
|
9096
|
-
const sourcePath = (0,
|
|
9786
|
+
const sourcePath = (0, import_node_path6.join)(historyRoot, sourceArchive, "messages.jsonl");
|
|
9097
9787
|
for (const text of await legacyMemoryTexts(sourcePath)) {
|
|
9098
9788
|
candidates.push({
|
|
9099
9789
|
comparableHash: sha256(comparableMemoryText(text)),
|
|
@@ -9161,7 +9851,7 @@ async function collectDurableMemoryHashes(root, hashes) {
|
|
|
9161
9851
|
return;
|
|
9162
9852
|
}
|
|
9163
9853
|
for (const entry of entries) {
|
|
9164
|
-
const path = (0,
|
|
9854
|
+
const path = (0, import_node_path6.join)(root, entry.name);
|
|
9165
9855
|
if (entry.isDirectory()) {
|
|
9166
9856
|
await collectDurableMemoryHashes(path, hashes);
|
|
9167
9857
|
continue;
|
|
@@ -9178,7 +9868,7 @@ async function collectDurableMemoryHashes(root, hashes) {
|
|
|
9178
9868
|
}
|
|
9179
9869
|
}
|
|
9180
9870
|
function isDurableMemoryPath(path) {
|
|
9181
|
-
return path.split(
|
|
9871
|
+
return path.split(import_node_path6.sep).includes("memories");
|
|
9182
9872
|
}
|
|
9183
9873
|
async function childDirectoryNames(path) {
|
|
9184
9874
|
let entries;
|
|
@@ -9222,10 +9912,10 @@ function legacySourceLabel(candidate) {
|
|
|
9222
9912
|
return `${candidate.sourceAccount}/${candidate.sourceSession}/${candidate.sourceArchive}`;
|
|
9223
9913
|
}
|
|
9224
9914
|
function localVikingDataRoot(config) {
|
|
9225
|
-
return (0,
|
|
9915
|
+
return (0, import_node_path6.join)(config.agentContextHome, "data", "viking");
|
|
9226
9916
|
}
|
|
9227
9917
|
function localUserMemoriesRoot(config) {
|
|
9228
|
-
return (0,
|
|
9918
|
+
return (0, import_node_path6.join)(localVikingDataRoot(config), config.account, "user", uriSegment(config.user), "memories");
|
|
9229
9919
|
}
|
|
9230
9920
|
function uniqueStrings(values) {
|
|
9231
9921
|
return [...new Set(values)].sort();
|
|
@@ -9241,14 +9931,15 @@ async function buildHandoff(options) {
|
|
|
9241
9931
|
const status = await gitValue(["status", "--short"], repoRoot) ?? "";
|
|
9242
9932
|
const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
|
|
9243
9933
|
const touchedFiles = await gitTouchedFiles(repoRoot);
|
|
9244
|
-
const repoName = (0,
|
|
9934
|
+
const repoName = (0, import_node_path6.basename)(repoRoot);
|
|
9935
|
+
const topicBranch = branch && branch !== "unknown" ? branch : "current";
|
|
9245
9936
|
const metadata = {
|
|
9246
9937
|
kind: "handoff",
|
|
9247
|
-
project:
|
|
9938
|
+
project: normalizeOptionalMetadata2(options.project) ?? repoName,
|
|
9248
9939
|
sourceAgentClient: options.sourceAgentClient ?? "codex",
|
|
9249
9940
|
status: "active",
|
|
9250
9941
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9251
|
-
topic:
|
|
9942
|
+
topic: handoffTopicForBranch(topicBranch, { timestamped: options.timestamped, topic: options.topic })
|
|
9252
9943
|
};
|
|
9253
9944
|
const bodyText = [
|
|
9254
9945
|
`repo: ${repoName}`,
|
|
@@ -9301,7 +9992,7 @@ function formatBlock(value, emptyValue) {
|
|
|
9301
9992
|
// src/update-check.ts
|
|
9302
9993
|
var import_node_child_process2 = require("node:child_process");
|
|
9303
9994
|
var import_promises6 = require("node:fs/promises");
|
|
9304
|
-
var
|
|
9995
|
+
var import_node_path7 = require("node:path");
|
|
9305
9996
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
9306
9997
|
var NPM_LATEST_URL = "https://registry.npmjs.org/threadnote/latest";
|
|
9307
9998
|
var FETCH_TIMEOUT_MS = 3e3;
|
|
@@ -9380,7 +10071,7 @@ async function readUpdateCache(cachePath) {
|
|
|
9380
10071
|
}
|
|
9381
10072
|
async function writeUpdateCache(cachePath, contents) {
|
|
9382
10073
|
try {
|
|
9383
|
-
await (0, import_promises6.mkdir)((0,
|
|
10074
|
+
await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(cachePath), { recursive: true });
|
|
9384
10075
|
await (0, import_promises6.writeFile)(cachePath, `${JSON.stringify(contents)}
|
|
9385
10076
|
`, { encoding: "utf8", mode: 384 });
|
|
9386
10077
|
} catch {
|
|
@@ -9404,14 +10095,14 @@ async function fetchLatestVersion() {
|
|
|
9404
10095
|
|
|
9405
10096
|
// src/version.ts
|
|
9406
10097
|
var import_node_fs4 = require("node:fs");
|
|
9407
|
-
var
|
|
10098
|
+
var import_node_path8 = require("node:path");
|
|
9408
10099
|
var cachedVersion;
|
|
9409
10100
|
function getThreadnoteVersion() {
|
|
9410
10101
|
if (cachedVersion !== void 0) {
|
|
9411
10102
|
return cachedVersion;
|
|
9412
10103
|
}
|
|
9413
10104
|
try {
|
|
9414
|
-
const packageJsonPath = (0,
|
|
10105
|
+
const packageJsonPath = (0, import_node_path8.join)(__dirname, "..", "package.json");
|
|
9415
10106
|
const parsed = JSON.parse((0, import_node_fs4.readFileSync)(packageJsonPath, "utf8"));
|
|
9416
10107
|
cachedVersion = typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : "unknown";
|
|
9417
10108
|
} catch {
|
|
@@ -9470,7 +10161,7 @@ async function runClaudeHooksInstall(options) {
|
|
|
9470
10161
|
console.log("\nRe-run with --apply to actually modify the file.");
|
|
9471
10162
|
return;
|
|
9472
10163
|
}
|
|
9473
|
-
await (0, import_promises7.mkdir)((0,
|
|
10164
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path), { recursive: true });
|
|
9474
10165
|
const serialized = `${JSON.stringify(next, void 0, 2)}
|
|
9475
10166
|
`;
|
|
9476
10167
|
await (0, import_promises7.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
|
|
@@ -9570,7 +10261,7 @@ async function hasManagedClaudeHooks() {
|
|
|
9570
10261
|
async function runPreCompactHook(config, options = {}) {
|
|
9571
10262
|
try {
|
|
9572
10263
|
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
9573
|
-
const project = repoRoot ? (0,
|
|
10264
|
+
const project = repoRoot ? (0, import_node_path9.basename)(repoRoot) : "general";
|
|
9574
10265
|
await runHandoff(config, {
|
|
9575
10266
|
blockers: "- none recorded",
|
|
9576
10267
|
dryRun: options.dryRun === true,
|
|
@@ -9594,7 +10285,7 @@ async function runSessionStartHook(config, options = {}) {
|
|
|
9594
10285
|
if (!repoRoot) {
|
|
9595
10286
|
return;
|
|
9596
10287
|
}
|
|
9597
|
-
const project = (0,
|
|
10288
|
+
const project = (0, import_node_path9.basename)(repoRoot);
|
|
9598
10289
|
await emitUpdateBannerIfOutdated(config);
|
|
9599
10290
|
process.stdout.write(`## Threadnote \u2014 latest context for ${project}
|
|
9600
10291
|
|
|
@@ -9603,7 +10294,8 @@ async function runSessionStartHook(config, options = {}) {
|
|
|
9603
10294
|
dryRun: options.dryRun === true,
|
|
9604
10295
|
inferScope: true,
|
|
9605
10296
|
nodeLimit: "5",
|
|
9606
|
-
|
|
10297
|
+
// Keep "current branch" here so recall enriches the query with local git/workspace terms.
|
|
10298
|
+
query: `${project} current branch latest handoff durable feature memory`
|
|
9607
10299
|
});
|
|
9608
10300
|
} catch (err) {
|
|
9609
10301
|
process.stderr.write(
|
|
@@ -9615,7 +10307,7 @@ async function runSessionStartHook(config, options = {}) {
|
|
|
9615
10307
|
async function emitUpdateBannerIfOutdated(config) {
|
|
9616
10308
|
try {
|
|
9617
10309
|
const result = await checkForThreadnoteUpdate({
|
|
9618
|
-
cachePath: (0,
|
|
10310
|
+
cachePath: (0, import_node_path9.join)(config.agentContextHome, ".update-state.json"),
|
|
9619
10311
|
currentVersion: getThreadnoteVersion()
|
|
9620
10312
|
});
|
|
9621
10313
|
if (!result || !result.outdated) {
|
|
@@ -9641,12 +10333,12 @@ async function emitUpdateBannerIfOutdated(config) {
|
|
|
9641
10333
|
|
|
9642
10334
|
// src/seeding.ts
|
|
9643
10335
|
var import_promises8 = require("node:fs/promises");
|
|
9644
|
-
var
|
|
10336
|
+
var import_node_path10 = require("node:path");
|
|
9645
10337
|
async function runSeed(config, options) {
|
|
9646
10338
|
const manifest = await readSeedManifest(config.manifestPath);
|
|
9647
10339
|
const ignorePatterns = await loadIgnorePatterns();
|
|
9648
10340
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
9649
|
-
const statePath = (0,
|
|
10341
|
+
const statePath = (0, import_node_path10.join)(config.agentContextHome, SEED_STATE_FILE);
|
|
9650
10342
|
const state = options.force === true ? { files: {}, version: 1 } : await readSeedState(statePath);
|
|
9651
10343
|
const projects = filterProjects(manifest.projects, options.only);
|
|
9652
10344
|
let importedCount = 0;
|
|
@@ -9726,13 +10418,13 @@ async function readSeedState(path) {
|
|
|
9726
10418
|
}
|
|
9727
10419
|
}
|
|
9728
10420
|
async function writeSeedState(path, state) {
|
|
9729
|
-
await ensureDirectory((0,
|
|
10421
|
+
await ensureDirectory((0, import_node_path10.dirname)(path), false);
|
|
9730
10422
|
await (0, import_promises8.writeFile)(path, `${JSON.stringify(state, void 0, 2)}
|
|
9731
10423
|
`, { encoding: "utf8", mode: 384 });
|
|
9732
10424
|
}
|
|
9733
10425
|
async function runInitManifest(config, options) {
|
|
9734
10426
|
const manifestPath = expandPath(
|
|
9735
|
-
options.path ?? process.env.THREADNOTE_MANIFEST ?? (0,
|
|
10427
|
+
options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path10.join)(config.agentContextHome, USER_MANIFEST_NAME)
|
|
9736
10428
|
);
|
|
9737
10429
|
const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
|
|
9738
10430
|
const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
|
|
@@ -9775,7 +10467,7 @@ async function runInitManifest(config, options) {
|
|
|
9775
10467
|
console.log(output2.trimEnd());
|
|
9776
10468
|
return;
|
|
9777
10469
|
}
|
|
9778
|
-
await ensureDirectory((0,
|
|
10470
|
+
await ensureDirectory((0, import_node_path10.dirname)(manifestPath), false);
|
|
9779
10471
|
await (0, import_promises8.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
9780
10472
|
await (0, import_promises8.chmod)(manifestPath, 384);
|
|
9781
10473
|
console.log(`Wrote manifest: ${manifestPath}`);
|
|
@@ -9813,7 +10505,7 @@ async function projectIdentity(path) {
|
|
|
9813
10505
|
}
|
|
9814
10506
|
}
|
|
9815
10507
|
function projectManifestForRepo(repoRoot, existingProjects) {
|
|
9816
|
-
const baseName = uriSegment((0,
|
|
10508
|
+
const baseName = uriSegment((0, import_node_path10.basename)(repoRoot));
|
|
9817
10509
|
const usedNames = new Set(existingProjects.map((project) => project.name));
|
|
9818
10510
|
const usedUris = new Set(existingProjects.map((project) => project.uri));
|
|
9819
10511
|
let name = baseName;
|
|
@@ -9835,7 +10527,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
|
|
|
9835
10527
|
for (const pattern of project.seed) {
|
|
9836
10528
|
const files = await resolveProjectPattern(projectRoot, pattern);
|
|
9837
10529
|
for (const filePath of files) {
|
|
9838
|
-
const relativePath = toPosixPath((0,
|
|
10530
|
+
const relativePath = toPosixPath((0, import_node_path10.relative)(projectRoot, filePath));
|
|
9839
10531
|
if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
|
|
9840
10532
|
continue;
|
|
9841
10533
|
}
|
|
@@ -9853,17 +10545,17 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
|
|
|
9853
10545
|
async function resolveProjectPattern(projectRoot, pattern) {
|
|
9854
10546
|
const normalizedPattern = toPosixPath(pattern);
|
|
9855
10547
|
if (!hasGlob(normalizedPattern)) {
|
|
9856
|
-
const filePath = (0,
|
|
10548
|
+
const filePath = (0, import_node_path10.join)(projectRoot, normalizedPattern);
|
|
9857
10549
|
return await isFile(filePath) ? [filePath] : [];
|
|
9858
10550
|
}
|
|
9859
10551
|
const globBase = getGlobBase(normalizedPattern);
|
|
9860
|
-
const basePath = (0,
|
|
10552
|
+
const basePath = (0, import_node_path10.join)(projectRoot, globBase);
|
|
9861
10553
|
if (!await exists(basePath)) {
|
|
9862
10554
|
return [];
|
|
9863
10555
|
}
|
|
9864
10556
|
const regex = globToRegExp(normalizedPattern);
|
|
9865
10557
|
const files = await walkFiles(basePath);
|
|
9866
|
-
return files.filter((filePath) => regex.test(toPosixPath((0,
|
|
10558
|
+
return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path10.relative)(projectRoot, filePath))));
|
|
9867
10559
|
}
|
|
9868
10560
|
async function prepareSeedFile(config, candidate, dryRun) {
|
|
9869
10561
|
const content = await (0, import_promises8.readFile)(candidate.filePath, "utf8");
|
|
@@ -9878,12 +10570,12 @@ async function prepareSeedFile(config, candidate, dryRun) {
|
|
|
9878
10570
|
if (redactedContent === content) {
|
|
9879
10571
|
return candidate.filePath;
|
|
9880
10572
|
}
|
|
9881
|
-
const redactedPath = (0,
|
|
10573
|
+
const redactedPath = (0, import_node_path10.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
|
|
9882
10574
|
if (dryRun) {
|
|
9883
10575
|
console.log(`Would write redacted copy: ${redactedPath}`);
|
|
9884
10576
|
return redactedPath;
|
|
9885
10577
|
}
|
|
9886
|
-
await ensureDirectory((0,
|
|
10578
|
+
await ensureDirectory((0, import_node_path10.dirname)(redactedPath), false);
|
|
9887
10579
|
await (0, import_promises8.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
|
|
9888
10580
|
await (0, import_promises8.chmod)(redactedPath, 384);
|
|
9889
10581
|
return redactedPath;
|
|
@@ -9941,10 +10633,10 @@ async function resolveAbsolutePattern(pattern) {
|
|
|
9941
10633
|
return files.filter((filePath) => regex.test(toPosixPath(filePath)));
|
|
9942
10634
|
}
|
|
9943
10635
|
function skillResourceUri(skill) {
|
|
9944
|
-
return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0,
|
|
10636
|
+
return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0, import_node_path10.basename)((0, import_node_path10.dirname)(skill.filePath)))}-${skill.hash.slice(0, 12)}.md`;
|
|
9945
10637
|
}
|
|
9946
10638
|
async function loadIgnorePatterns() {
|
|
9947
|
-
const raw = await (0, import_promises8.readFile)((0,
|
|
10639
|
+
const raw = await (0, import_promises8.readFile)((0, import_node_path10.join)(toolRoot(), ".threadnoteignore"), "utf8");
|
|
9948
10640
|
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
9949
10641
|
}
|
|
9950
10642
|
function matchesIgnore(relativePath, patterns) {
|
|
@@ -10019,7 +10711,7 @@ var import_node_child_process3 = require("node:child_process");
|
|
|
10019
10711
|
var import_node_fs6 = require("node:fs");
|
|
10020
10712
|
var import_promises11 = require("node:fs/promises");
|
|
10021
10713
|
var import_node_os6 = require("node:os");
|
|
10022
|
-
var
|
|
10714
|
+
var import_node_path12 = require("node:path");
|
|
10023
10715
|
var import_node_process2 = require("node:process");
|
|
10024
10716
|
var import_promises12 = require("node:readline/promises");
|
|
10025
10717
|
|
|
@@ -10027,7 +10719,7 @@ var import_promises12 = require("node:readline/promises");
|
|
|
10027
10719
|
var import_node_fs5 = require("node:fs");
|
|
10028
10720
|
var import_promises9 = require("node:fs/promises");
|
|
10029
10721
|
var import_node_os5 = require("node:os");
|
|
10030
|
-
var
|
|
10722
|
+
var import_node_path11 = require("node:path");
|
|
10031
10723
|
var import_promises10 = require("node:readline/promises");
|
|
10032
10724
|
var import_node_process = require("node:process");
|
|
10033
10725
|
var NPM_PACKAGE_NAME = "threadnote";
|
|
@@ -10242,7 +10934,7 @@ async function waitForOpenVikingHealthy(config, timeoutMs) {
|
|
|
10242
10934
|
return isOpenVikingHealthy(config);
|
|
10243
10935
|
}
|
|
10244
10936
|
function launchAgentPlistPath() {
|
|
10245
|
-
return (0,
|
|
10937
|
+
return (0, import_node_path11.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
|
|
10246
10938
|
}
|
|
10247
10939
|
async function isLaunchAgentInstalled() {
|
|
10248
10940
|
if (process.platform !== "darwin") {
|
|
@@ -10278,7 +10970,7 @@ async function getUpdateInfo(config, options) {
|
|
|
10278
10970
|
};
|
|
10279
10971
|
}
|
|
10280
10972
|
async function currentPackageVersion() {
|
|
10281
|
-
const rawPackage = await (0, import_promises9.readFile)((0,
|
|
10973
|
+
const rawPackage = await (0, import_promises9.readFile)((0, import_node_path11.join)(toolRoot(), "package.json"), "utf8");
|
|
10282
10974
|
const parsed = JSON.parse(rawPackage);
|
|
10283
10975
|
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
10284
10976
|
throw new Error("Could not read current threadnote package version.");
|
|
@@ -10332,7 +11024,7 @@ async function writeUpdateCache2(config, cache) {
|
|
|
10332
11024
|
`, { encoding: "utf8", mode: 384 });
|
|
10333
11025
|
}
|
|
10334
11026
|
function updateCachePath(config) {
|
|
10335
|
-
return (0,
|
|
11027
|
+
return (0, import_node_path11.join)(config.agentContextHome, "update-check.json");
|
|
10336
11028
|
}
|
|
10337
11029
|
async function runApplicablePostUpdateMigrations(config, options) {
|
|
10338
11030
|
const state = await readPostUpdateState(config);
|
|
@@ -10396,7 +11088,7 @@ async function applicablePostUpdateMigrations(config, options) {
|
|
|
10396
11088
|
return applicable;
|
|
10397
11089
|
}
|
|
10398
11090
|
async function readPostUpdateMigrations() {
|
|
10399
|
-
const raw = await readFileIfExists((0,
|
|
11091
|
+
const raw = await readFileIfExists((0, import_node_path11.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
|
|
10400
11092
|
if (!raw) {
|
|
10401
11093
|
return [];
|
|
10402
11094
|
}
|
|
@@ -10474,7 +11166,7 @@ async function writePostUpdateState(config, state) {
|
|
|
10474
11166
|
`, { encoding: "utf8", mode: 384 });
|
|
10475
11167
|
}
|
|
10476
11168
|
function postUpdateStatePath(config) {
|
|
10477
|
-
return (0,
|
|
11169
|
+
return (0, import_node_path11.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
|
|
10478
11170
|
}
|
|
10479
11171
|
async function resolveUpdateRuntime(runtime) {
|
|
10480
11172
|
if (runtime !== "auto") {
|
|
@@ -10504,22 +11196,14 @@ async function runtimeThreadnoteBin(runtime) {
|
|
|
10504
11196
|
if (runtime === "npm") {
|
|
10505
11197
|
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
10506
11198
|
const prefix = result.stdout.trim();
|
|
10507
|
-
return prefix ? (0,
|
|
11199
|
+
return prefix ? (0, import_node_path11.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
10508
11200
|
}
|
|
10509
11201
|
if (runtime === "bun") {
|
|
10510
11202
|
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
10511
11203
|
const binDir = result.stdout.trim();
|
|
10512
|
-
return binDir ? (0,
|
|
10513
|
-
}
|
|
10514
|
-
return (0, import_node_path10.join)(process.env.DENO_INSTALL ?? (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
|
|
10515
|
-
}
|
|
10516
|
-
async function isExecutable(path) {
|
|
10517
|
-
try {
|
|
10518
|
-
await (0, import_promises9.access)(path, import_node_fs5.constants.X_OK);
|
|
10519
|
-
return true;
|
|
10520
|
-
} catch (_err) {
|
|
10521
|
-
return false;
|
|
11204
|
+
return binDir ? (0, import_node_path11.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
10522
11205
|
}
|
|
11206
|
+
return (0, import_node_path11.join)(process.env.DENO_INSTALL ?? (0, import_node_path11.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
|
|
10523
11207
|
}
|
|
10524
11208
|
function updatePackageCommand(runtime, registry) {
|
|
10525
11209
|
if (runtime === "npm") {
|
|
@@ -10564,7 +11248,7 @@ async function runDoctor(config, options) {
|
|
|
10564
11248
|
checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
|
|
10565
11249
|
checks.push(await commandCheck("node", ["--version"]));
|
|
10566
11250
|
checks.push(await commandCheck("python3", ["--version"]));
|
|
10567
|
-
checks.push(await
|
|
11251
|
+
checks.push(await openVikingServerCheck());
|
|
10568
11252
|
checks.push(await firstCommandCheck("openviking cli", ["ov", "openviking"], ["--help"]));
|
|
10569
11253
|
checks.push(await localEmbeddingCheck());
|
|
10570
11254
|
checks.push(await pythonSystemCertificatesCheck());
|
|
@@ -10574,9 +11258,9 @@ async function runDoctor(config, options) {
|
|
|
10574
11258
|
checks.push(await commandShimCheck());
|
|
10575
11259
|
checks.push(...await userAgentInstructionsChecks());
|
|
10576
11260
|
checks.push(await manifestCheck(config.manifestPath));
|
|
10577
|
-
checks.push(await fileCheck((0,
|
|
10578
|
-
checks.push(await fileCheck((0,
|
|
10579
|
-
checks.push(await fileCheck((0,
|
|
11261
|
+
checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
|
|
11262
|
+
checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
|
|
11263
|
+
checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
|
|
10580
11264
|
checks.push(await healthCheck(config));
|
|
10581
11265
|
for (const check of checks) {
|
|
10582
11266
|
console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
|
|
@@ -10593,12 +11277,13 @@ async function runInstall(config, options) {
|
|
|
10593
11277
|
const repairInvalidConfigs = options.repairInvalidConfigs === true;
|
|
10594
11278
|
const dryRun = options.dryRun === true;
|
|
10595
11279
|
await ensureDirectory(config.agentContextHome, dryRun);
|
|
10596
|
-
await ensureDirectory((0,
|
|
10597
|
-
await ensureDirectory((0,
|
|
10598
|
-
await ensureDirectory((0,
|
|
11280
|
+
await ensureDirectory((0, import_node_path12.join)(config.agentContextHome, "logs"), dryRun);
|
|
11281
|
+
await ensureDirectory((0, import_node_path12.join)(config.agentContextHome, "redacted"), dryRun);
|
|
11282
|
+
await ensureDirectory((0, import_node_path12.join)(config.agentContextHome, "mcp"), dryRun);
|
|
10599
11283
|
await installCommandShim(dryRun);
|
|
10600
11284
|
await installUserAgentInstructions(dryRun);
|
|
10601
|
-
const serverPath = await
|
|
11285
|
+
const serverPath = await findOpenVikingServer();
|
|
11286
|
+
let serverInstallRan = false;
|
|
10602
11287
|
if (serverPath) {
|
|
10603
11288
|
console.log(`OpenViking server already installed: ${serverPath}`);
|
|
10604
11289
|
const localEmbeddingMissing = await hasLocalEmbeddingDependency(serverPath) === false;
|
|
@@ -10606,6 +11291,7 @@ async function runInstall(config, options) {
|
|
|
10606
11291
|
if (options.force === true) {
|
|
10607
11292
|
console.log(`Reinstalling OpenViking at pinned version ${config.openVikingVersion} (--force).`);
|
|
10608
11293
|
await runInstallCommands(config, options.packageManager, true, dryRun);
|
|
11294
|
+
serverInstallRan = true;
|
|
10609
11295
|
} else if (localEmbeddingMissing) {
|
|
10610
11296
|
const repairReasons = [];
|
|
10611
11297
|
repairReasons.push("local embedding extra is missing");
|
|
@@ -10614,6 +11300,7 @@ async function runInstall(config, options) {
|
|
|
10614
11300
|
}
|
|
10615
11301
|
console.log(`OpenViking install needs repair: ${repairReasons.join("; ")}.`);
|
|
10616
11302
|
await runInstallCommands(config, options.packageManager, true, dryRun);
|
|
11303
|
+
serverInstallRan = true;
|
|
10617
11304
|
} else if (pythonSystemCertificatesMissing) {
|
|
10618
11305
|
console.log("OpenViking install needs repair: Python system certificate bridge is missing.");
|
|
10619
11306
|
const installCommand = await getPythonSystemCertificatesInstallCommand(serverPath);
|
|
@@ -10621,20 +11308,25 @@ async function runInstall(config, options) {
|
|
|
10621
11308
|
}
|
|
10622
11309
|
} else {
|
|
10623
11310
|
await runInstallCommands(config, options.packageManager, false, dryRun);
|
|
11311
|
+
serverInstallRan = true;
|
|
11312
|
+
}
|
|
11313
|
+
const resolvedServerPath = serverInstallRan ? await findOpenVikingServer() : serverPath;
|
|
11314
|
+
if (resolvedServerPath && !dryRun) {
|
|
11315
|
+
await maybePrintOpenVikingPathHint(resolvedServerPath);
|
|
10624
11316
|
}
|
|
10625
11317
|
await writeTemplateIfMissing({
|
|
10626
11318
|
config,
|
|
10627
|
-
destinationPath: (0,
|
|
11319
|
+
destinationPath: (0, import_node_path12.join)(config.agentContextHome, "ov.conf"),
|
|
10628
11320
|
dryRun,
|
|
10629
11321
|
shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
10630
|
-
templatePath: (0,
|
|
11322
|
+
templatePath: (0, import_node_path12.join)(toolRoot(), "config", "ov.conf.template.json")
|
|
10631
11323
|
});
|
|
10632
11324
|
await writeTemplateIfMissing({
|
|
10633
11325
|
config,
|
|
10634
|
-
destinationPath: (0,
|
|
11326
|
+
destinationPath: (0, import_node_path12.join)(config.agentContextHome, "ovcli.conf"),
|
|
10635
11327
|
dryRun,
|
|
10636
11328
|
shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
10637
|
-
templatePath: (0,
|
|
11329
|
+
templatePath: (0, import_node_path12.join)(toolRoot(), "config", "ovcli.conf.template.json")
|
|
10638
11330
|
});
|
|
10639
11331
|
if (options.start !== false) {
|
|
10640
11332
|
const healthy = await repairServerHealth(config, dryRun);
|
|
@@ -10688,7 +11380,7 @@ async function runUninstall(config, options) {
|
|
|
10688
11380
|
}
|
|
10689
11381
|
console.log("Uninstalling local Threadnote setup.");
|
|
10690
11382
|
await runStop(config, { dryRun });
|
|
10691
|
-
await removePathIfExists((0,
|
|
11383
|
+
await removePathIfExists((0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
|
|
10692
11384
|
await removeLaunchAgent(dryRun);
|
|
10693
11385
|
await removeMcpConfigs(options.mcp ?? "available", dryRun);
|
|
10694
11386
|
await removeMcpSnippets(config, dryRun);
|
|
@@ -10745,7 +11437,7 @@ async function repairManifest(config, dryRun) {
|
|
|
10745
11437
|
console.log(output2.trimEnd());
|
|
10746
11438
|
return;
|
|
10747
11439
|
}
|
|
10748
|
-
await ensureDirectory((0,
|
|
11440
|
+
await ensureDirectory((0, import_node_path12.dirname)(config.manifestPath), false);
|
|
10749
11441
|
const currentContent = await readFileIfExists(config.manifestPath);
|
|
10750
11442
|
if (currentContent !== void 0) {
|
|
10751
11443
|
const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
|
|
@@ -10757,6 +11449,64 @@ async function repairManifest(config, dryRun) {
|
|
|
10757
11449
|
await (0, import_promises11.chmod)(config.manifestPath, 384);
|
|
10758
11450
|
console.log(`Wrote replacement manifest: ${config.manifestPath}`);
|
|
10759
11451
|
}
|
|
11452
|
+
async function findOpenVikingServer() {
|
|
11453
|
+
const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
11454
|
+
if (onPath) {
|
|
11455
|
+
return onPath;
|
|
11456
|
+
}
|
|
11457
|
+
for (const candidateDir of await openVikingServerCandidateDirs()) {
|
|
11458
|
+
const candidate = (0, import_node_path12.join)(candidateDir, OPENVIKING_SERVER_COMMAND);
|
|
11459
|
+
if (await isExecutable(candidate)) {
|
|
11460
|
+
return candidate;
|
|
11461
|
+
}
|
|
11462
|
+
}
|
|
11463
|
+
return void 0;
|
|
11464
|
+
}
|
|
11465
|
+
var candidateDirsPromise;
|
|
11466
|
+
async function openVikingServerCandidateDirs() {
|
|
11467
|
+
if (!candidateDirsPromise) {
|
|
11468
|
+
candidateDirsPromise = computeOpenVikingServerCandidateDirs();
|
|
11469
|
+
}
|
|
11470
|
+
return candidateDirsPromise;
|
|
11471
|
+
}
|
|
11472
|
+
async function computeOpenVikingServerCandidateDirs() {
|
|
11473
|
+
const dirs = [];
|
|
11474
|
+
const uv = await findExecutable(["uv"]);
|
|
11475
|
+
if (uv) {
|
|
11476
|
+
const result = await runCommand(uv, ["tool", "dir", "--bin"], { allowFailure: true });
|
|
11477
|
+
if (result.exitCode === 0) {
|
|
11478
|
+
const dir = result.stdout.trim();
|
|
11479
|
+
if (dir) {
|
|
11480
|
+
dirs.push(dir);
|
|
11481
|
+
}
|
|
11482
|
+
}
|
|
11483
|
+
}
|
|
11484
|
+
if (process.env.UV_TOOL_BIN_DIR) {
|
|
11485
|
+
dirs.push(process.env.UV_TOOL_BIN_DIR);
|
|
11486
|
+
}
|
|
11487
|
+
dirs.push(expandPath("~/.local/bin"));
|
|
11488
|
+
return Array.from(new Set(dirs));
|
|
11489
|
+
}
|
|
11490
|
+
async function requireOpenVikingServer() {
|
|
11491
|
+
const resolved = await findOpenVikingServer();
|
|
11492
|
+
if (!resolved) {
|
|
11493
|
+
throw new Error(
|
|
11494
|
+
`${OPENVIKING_SERVER_COMMAND} was not found in PATH, uv tool bin dir, $UV_TOOL_BIN_DIR, or ~/.local/bin. Run \`threadnote install\` first.`
|
|
11495
|
+
);
|
|
11496
|
+
}
|
|
11497
|
+
return resolved;
|
|
11498
|
+
}
|
|
11499
|
+
async function maybePrintOpenVikingPathHint(serverPath) {
|
|
11500
|
+
const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
11501
|
+
if (onPath) {
|
|
11502
|
+
return;
|
|
11503
|
+
}
|
|
11504
|
+
const binDir = (0, import_node_path12.dirname)(serverPath);
|
|
11505
|
+
const rcHint = suggestedShellRc(process.env.SHELL, (0, import_node_os6.platform)());
|
|
11506
|
+
console.log(
|
|
11507
|
+
`Note: ${serverPath} is installed but ${binDir} is not on this shell's PATH. Add \`export PATH="${binDir}:$PATH"\` to ${rcHint} so other tools can find openviking-server.`
|
|
11508
|
+
);
|
|
11509
|
+
}
|
|
10760
11510
|
async function repairServerHealth(config, dryRun) {
|
|
10761
11511
|
const existingHealth = await readOpenVikingHealthIfAvailable(config, 800);
|
|
10762
11512
|
if (existingHealth) {
|
|
@@ -10777,7 +11527,7 @@ async function runStart(config, options) {
|
|
|
10777
11527
|
await installLaunchAgent(config, options.dryRun === true);
|
|
10778
11528
|
return;
|
|
10779
11529
|
}
|
|
10780
|
-
const server = options.dryRun === true ? await
|
|
11530
|
+
const server = options.dryRun === true ? await findOpenVikingServer() ?? OPENVIKING_SERVER_COMMAND : await requireOpenVikingServer();
|
|
10781
11531
|
const args = openVikingServerArgs(config);
|
|
10782
11532
|
if (options.dryRun === true) {
|
|
10783
11533
|
console.log(formatShellCommand(server, args));
|
|
@@ -10795,7 +11545,7 @@ async function runStart(config, options) {
|
|
|
10795
11545
|
);
|
|
10796
11546
|
}
|
|
10797
11547
|
const logPath = openVikingLogPath(config);
|
|
10798
|
-
await ensureDirectory((0,
|
|
11548
|
+
await ensureDirectory((0, import_node_path12.dirname)(logPath), false);
|
|
10799
11549
|
if (options.foreground === true) {
|
|
10800
11550
|
const result = await runInteractive(server, args);
|
|
10801
11551
|
process.exitCode = result;
|
|
@@ -10804,7 +11554,7 @@ async function runStart(config, options) {
|
|
|
10804
11554
|
const logFd = (0, import_node_fs6.openSync)(logPath, "a");
|
|
10805
11555
|
const child = spawnDetachedServerWithLog(server, args, logFd);
|
|
10806
11556
|
child.unref();
|
|
10807
|
-
await (0, import_promises11.writeFile)((0,
|
|
11557
|
+
await (0, import_promises11.writeFile)((0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
|
|
10808
11558
|
`, "utf8");
|
|
10809
11559
|
const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
|
|
10810
11560
|
if (health) {
|
|
@@ -10837,7 +11587,7 @@ async function runStop(config, options) {
|
|
|
10837
11587
|
console.log(`No LaunchAgent found: ${launchAgentPath}`);
|
|
10838
11588
|
}
|
|
10839
11589
|
}
|
|
10840
|
-
const pidPath = (0,
|
|
11590
|
+
const pidPath = (0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid");
|
|
10841
11591
|
const pidText = await readFileIfExists(pidPath);
|
|
10842
11592
|
if (!pidText) {
|
|
10843
11593
|
console.log("No pid file found for detached OpenViking server.");
|
|
@@ -10871,6 +11621,21 @@ async function commandCheck(name, args) {
|
|
|
10871
11621
|
detail: firstLine(result.stdout || result.stderr) || executable
|
|
10872
11622
|
};
|
|
10873
11623
|
}
|
|
11624
|
+
async function openVikingServerCheck() {
|
|
11625
|
+
const name = OPENVIKING_SERVER_COMMAND;
|
|
11626
|
+
const executable = await findOpenVikingServer();
|
|
11627
|
+
if (!executable) {
|
|
11628
|
+
return { name, status: "fail", detail: "missing; install will fetch it via uv or pipx" };
|
|
11629
|
+
}
|
|
11630
|
+
const result = await runCommand(executable, ["--help"], { allowFailure: true });
|
|
11631
|
+
const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
11632
|
+
const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path12.dirname)(executable)} to PATH)`;
|
|
11633
|
+
return {
|
|
11634
|
+
name,
|
|
11635
|
+
status: result.exitCode === 0 ? "ok" : "warn",
|
|
11636
|
+
detail: result.exitCode === 0 ? detail : firstLine(result.stderr || result.stdout) || detail
|
|
11637
|
+
};
|
|
11638
|
+
}
|
|
10874
11639
|
async function commandPresenceCheck(name, args) {
|
|
10875
11640
|
const executable = await findExecutable([name]);
|
|
10876
11641
|
if (!executable) {
|
|
@@ -10899,7 +11664,7 @@ async function firstCommandCheck(name, commands, args) {
|
|
|
10899
11664
|
return { name, status: "fail", detail: `none found: ${commands.join(", ")}` };
|
|
10900
11665
|
}
|
|
10901
11666
|
async function localEmbeddingCheck() {
|
|
10902
|
-
const serverPath = await
|
|
11667
|
+
const serverPath = await findOpenVikingServer();
|
|
10903
11668
|
if (!serverPath) {
|
|
10904
11669
|
return { name: "local embedding extra", status: "warn", detail: "openviking-server missing" };
|
|
10905
11670
|
}
|
|
@@ -10918,7 +11683,7 @@ async function localEmbeddingCheck() {
|
|
|
10918
11683
|
};
|
|
10919
11684
|
}
|
|
10920
11685
|
async function pythonSystemCertificatesCheck() {
|
|
10921
|
-
const serverPath = await
|
|
11686
|
+
const serverPath = await findOpenVikingServer();
|
|
10922
11687
|
if (!serverPath) {
|
|
10923
11688
|
return { name: "python system certs", status: "warn", detail: "openviking-server missing" };
|
|
10924
11689
|
}
|
|
@@ -10933,7 +11698,7 @@ async function pythonSystemCertificatesCheck() {
|
|
|
10933
11698
|
};
|
|
10934
11699
|
}
|
|
10935
11700
|
async function commandShimCheck() {
|
|
10936
|
-
const shimPath = (0,
|
|
11701
|
+
const shimPath = (0, import_node_path12.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
10937
11702
|
const content = await readFileIfExists(shimPath);
|
|
10938
11703
|
if (content === void 0) {
|
|
10939
11704
|
return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
|
|
@@ -11003,7 +11768,7 @@ async function siblingPythonForExecutable(executablePath) {
|
|
|
11003
11768
|
} catch (_err) {
|
|
11004
11769
|
return void 0;
|
|
11005
11770
|
}
|
|
11006
|
-
const pythonPath = (0,
|
|
11771
|
+
const pythonPath = (0, import_node_path12.join)((0, import_node_path12.dirname)(resolvedPath), "python");
|
|
11007
11772
|
return await exists(pythonPath) ? pythonPath : void 0;
|
|
11008
11773
|
}
|
|
11009
11774
|
async function manifestCheck(path) {
|
|
@@ -11099,6 +11864,7 @@ async function installUv() {
|
|
|
11099
11864
|
console.log(result.stdout.trim());
|
|
11100
11865
|
}
|
|
11101
11866
|
if (await findExecutable(["uv"])) {
|
|
11867
|
+
candidateDirsPromise = void 0;
|
|
11102
11868
|
return true;
|
|
11103
11869
|
}
|
|
11104
11870
|
} else {
|
|
@@ -11115,6 +11881,7 @@ async function installUv() {
|
|
|
11115
11881
|
console.log(result.stdout.trim());
|
|
11116
11882
|
}
|
|
11117
11883
|
if (await findExecutable(["uv"])) {
|
|
11884
|
+
candidateDirsPromise = void 0;
|
|
11118
11885
|
return true;
|
|
11119
11886
|
}
|
|
11120
11887
|
console.warn(
|
|
@@ -11221,14 +11988,14 @@ async function writeTemplateIfMissing(options) {
|
|
|
11221
11988
|
console.log(`Would write ${options.destinationPath}`);
|
|
11222
11989
|
return;
|
|
11223
11990
|
}
|
|
11224
|
-
await ensureDirectory((0,
|
|
11991
|
+
await ensureDirectory((0, import_node_path12.dirname)(options.destinationPath), false);
|
|
11225
11992
|
await (0, import_promises11.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
|
|
11226
11993
|
await (0, import_promises11.chmod)(options.destinationPath, 384);
|
|
11227
11994
|
console.log(`Wrote ${options.destinationPath}`);
|
|
11228
11995
|
}
|
|
11229
11996
|
async function installCommandShim(dryRun) {
|
|
11230
11997
|
const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
|
|
11231
|
-
const shimPath = (0,
|
|
11998
|
+
const shimPath = (0, import_node_path12.join)(binDir, "threadnote");
|
|
11232
11999
|
const existingContent = await readFileIfExists(shimPath);
|
|
11233
12000
|
if (existingContent && !isManagedCommandShim(existingContent)) {
|
|
11234
12001
|
console.log(`WARN not overwriting existing command shim: ${shimPath}`);
|
|
@@ -11249,7 +12016,7 @@ async function installCommandShim(dryRun) {
|
|
|
11249
12016
|
console.log(`Wrote command shim: ${shimPath}`);
|
|
11250
12017
|
}
|
|
11251
12018
|
async function removeCommandShim(dryRun) {
|
|
11252
|
-
const shimPath = (0,
|
|
12019
|
+
const shimPath = (0, import_node_path12.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
11253
12020
|
const content = await readFileIfExists(shimPath);
|
|
11254
12021
|
if (content === void 0) {
|
|
11255
12022
|
console.log(`Already absent: ${shimPath}`);
|
|
@@ -11283,7 +12050,7 @@ async function installUserAgentInstructions(dryRun) {
|
|
|
11283
12050
|
console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
|
|
11284
12051
|
continue;
|
|
11285
12052
|
}
|
|
11286
|
-
await ensureDirectory((0,
|
|
12053
|
+
await ensureDirectory((0, import_node_path12.dirname)(targetPath), false);
|
|
11287
12054
|
await (0, import_promises11.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
11288
12055
|
console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
|
|
11289
12056
|
}
|
|
@@ -11341,7 +12108,7 @@ async function renderUserAgentInstructions(target) {
|
|
|
11341
12108
|
].join("\n");
|
|
11342
12109
|
}
|
|
11343
12110
|
async function renderUserAgentInstructionsBlock() {
|
|
11344
|
-
const instructions = (await (0, import_promises11.readFile)((0,
|
|
12111
|
+
const instructions = (await (0, import_promises11.readFile)((0, import_node_path12.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
|
|
11345
12112
|
return `${USER_INSTRUCTIONS_START_MARKER}
|
|
11346
12113
|
${instructions}
|
|
11347
12114
|
${USER_INSTRUCTIONS_END_MARKER}`;
|
|
@@ -11419,18 +12186,28 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
11419
12186
|
if ((0, import_node_os6.platform)() !== "darwin") {
|
|
11420
12187
|
throw new Error("launchd autostart is only supported on macOS.");
|
|
11421
12188
|
}
|
|
11422
|
-
const
|
|
12189
|
+
const resolvedServer = await findOpenVikingServer();
|
|
12190
|
+
if (!resolvedServer && !dryRun) {
|
|
12191
|
+
throw new Error(
|
|
12192
|
+
`Cannot install LaunchAgent: ${OPENVIKING_SERVER_COMMAND} was not found in PATH, uv tool bin dir, $UV_TOOL_BIN_DIR, or ~/.local/bin. Run \`threadnote install\` first.`
|
|
12193
|
+
);
|
|
12194
|
+
}
|
|
12195
|
+
const source = (0, import_node_path12.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
|
|
11423
12196
|
const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
11424
|
-
const rendered = renderTemplate(await (0, import_promises11.readFile)(source, "utf8"), config
|
|
12197
|
+
const rendered = renderTemplate(await (0, import_promises11.readFile)(source, "utf8"), config, {
|
|
12198
|
+
OPENVIKING_SERVER_PATH: resolvedServer ?? OPENVIKING_SERVER_COMMAND
|
|
12199
|
+
});
|
|
11425
12200
|
if (dryRun) {
|
|
12201
|
+
const resolutionDetail = resolvedServer ?? `<not found; would use bare \`${OPENVIKING_SERVER_COMMAND}\`>`;
|
|
12202
|
+
console.log(`Resolved openviking-server: ${resolutionDetail}`);
|
|
11426
12203
|
console.log(`Would write ${destination}`);
|
|
11427
12204
|
console.log(`Would run: launchctl unload ${destination}`);
|
|
11428
12205
|
console.log(`Would run: launchctl load ${destination}`);
|
|
11429
12206
|
console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
|
|
11430
12207
|
return;
|
|
11431
12208
|
}
|
|
11432
|
-
await ensureDirectory((0,
|
|
11433
|
-
await ensureDirectory((0,
|
|
12209
|
+
await ensureDirectory((0, import_node_path12.dirname)(destination), false);
|
|
12210
|
+
await ensureDirectory((0, import_node_path12.dirname)(openVikingLogPath(config)), false);
|
|
11434
12211
|
await (0, import_promises11.writeFile)(destination, rendered, "utf8");
|
|
11435
12212
|
await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
|
|
11436
12213
|
await maybeRun(false, "launchctl", ["load", destination]);
|
|
@@ -11494,7 +12271,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
|
|
|
11494
12271
|
if (typeof parsed.default_user !== "string") {
|
|
11495
12272
|
return false;
|
|
11496
12273
|
}
|
|
11497
|
-
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0,
|
|
12274
|
+
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path12.join)(config.agentContextHome, "data")) {
|
|
11498
12275
|
return false;
|
|
11499
12276
|
}
|
|
11500
12277
|
return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
|
|
@@ -11614,16 +12391,19 @@ async function main() {
|
|
|
11614
12391
|
program2.command("migrate-lifecycle").description("Move clear legacy handoff memories into lifecycle-aware archive paths").option("--apply", "Perform the migration; without this, prints a dry run").option("--dry-run", "Print migration actions without writing or removing memories").option("--limit <count>", "Maximum number of legacy handoffs to migrate").action(async (options) => {
|
|
11615
12392
|
await runMigrateLifecycle(getRuntimeConfig(program2), options);
|
|
11616
12393
|
});
|
|
11617
|
-
program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in exact
|
|
12394
|
+
program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in exact memory/resource matches").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
|
|
11618
12395
|
await runRecall(getRuntimeConfig(program2), options);
|
|
11619
12396
|
});
|
|
12397
|
+
program2.command("compact").description("Plan or apply scoped memory hygiene for active personal memories").requiredOption("--project <name>", "Project/repo namespace to inspect").option("--apply", "Apply the compact plan; without this, prints a dry run").option("--dry-run", "Print the compact plan without changing anything").option("--kind <kind>", "durable, handoff, or incident", parseCompactKind).option("--topic <name>", "Stable topic name to inspect").action(async (options) => {
|
|
12398
|
+
await runCompact(getRuntimeConfig(program2), options);
|
|
12399
|
+
});
|
|
11620
12400
|
program2.command("read").description("Read a viking:// URI returned by recall or list").argument("<uri>", "viking:// URI to read").option("--dry-run", "Print ov command without reading").action(async (uri, options) => {
|
|
11621
12401
|
await runRead(getRuntimeConfig(program2), uri, options);
|
|
11622
12402
|
});
|
|
11623
12403
|
program2.command("list").alias("ls").description("List a viking:// directory").argument("[uri]", "viking:// URI to list", "viking://").option("-a, --all", "Show hidden files such as .abstract.md and .overview.md").option("--dry-run", "Print ov command without listing").option("-n, --node-limit <count>", "Maximum number of nodes to list").option("-r, --recursive", "List subdirectories recursively").option("-s, --simple", "Print only paths").action(async (uri, options) => {
|
|
11624
12404
|
await runList(getRuntimeConfig(program2), uri, options);
|
|
11625
12405
|
});
|
|
11626
|
-
program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
|
|
12406
|
+
program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--timestamped", "Store a historical timestamped handoff instead of updating the current branch handoff").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
|
|
11627
12407
|
await runHandoff(getRuntimeConfig(program2), options);
|
|
11628
12408
|
});
|
|
11629
12409
|
program2.command("archive").description("Move a memory into the archived lifecycle tree, then remove the original after the archive is stored").argument("<uri>", "viking:// memory URI to archive").option("--dry-run", "Print archive content and ov commands without changing anything").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind).option("--project <name>", "Override inferred project/repo namespace").option("--topic <name>", "Override inferred topic").action(async (uri, options) => {
|