threadnote 0.7.5 → 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/dist/mcp_server.cjs +776 -36
- package/dist/threadnote.cjs +815 -145
- package/docs/agent-instructions.md +24 -5
- package/docs/migration.md +4 -2
- package/package.json +1 -1
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");
|
|
@@ -3958,6 +3958,57 @@ function portablePath(path) {
|
|
|
3958
3958
|
function getInvocationCwd() {
|
|
3959
3959
|
return process.env.THREADNOTE_CALLER_CWD ?? process.cwd();
|
|
3960
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
|
+
}
|
|
3961
4012
|
function toPosixPath(path) {
|
|
3962
4013
|
return path.split(import_node_path.sep).join("/");
|
|
3963
4014
|
}
|
|
@@ -3981,19 +4032,29 @@ function exactRecallTerms(query) {
|
|
|
3981
4032
|
"branch",
|
|
3982
4033
|
"case",
|
|
3983
4034
|
"current",
|
|
4035
|
+
"durable",
|
|
3984
4036
|
"find",
|
|
4037
|
+
"feature",
|
|
4038
|
+
"features",
|
|
3985
4039
|
"handoff",
|
|
3986
4040
|
"issue",
|
|
3987
4041
|
"issues",
|
|
4042
|
+
"knowledge",
|
|
3988
4043
|
"latest",
|
|
3989
4044
|
"memory",
|
|
3990
4045
|
"memories",
|
|
4046
|
+
"project",
|
|
3991
4047
|
"recall",
|
|
4048
|
+
"repo",
|
|
4049
|
+
"repository",
|
|
3992
4050
|
"related",
|
|
3993
4051
|
"search",
|
|
3994
|
-
"
|
|
4052
|
+
"stored",
|
|
3995
4053
|
"this",
|
|
3996
|
-
"
|
|
4054
|
+
"the",
|
|
4055
|
+
"with",
|
|
4056
|
+
"workspace",
|
|
4057
|
+
"worktree"
|
|
3997
4058
|
]);
|
|
3998
4059
|
const seen = /* @__PURE__ */ new Set();
|
|
3999
4060
|
const terms = [];
|
|
@@ -4541,7 +4602,7 @@ function existsSyncDirectory(path) {
|
|
|
4541
4602
|
|
|
4542
4603
|
// src/memory.ts
|
|
4543
4604
|
var import_promises5 = require("node:fs/promises");
|
|
4544
|
-
var
|
|
4605
|
+
var import_node_path6 = require("node:path");
|
|
4545
4606
|
|
|
4546
4607
|
// src/manifest.ts
|
|
4547
4608
|
var import_promises3 = require("node:fs/promises");
|
|
@@ -7239,10 +7300,445 @@ function readStringArray(object, key) {
|
|
|
7239
7300
|
return value;
|
|
7240
7301
|
}
|
|
7241
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
|
+
|
|
7242
7738
|
// src/runtime.ts
|
|
7243
7739
|
var import_node_fs3 = require("node:fs");
|
|
7244
7740
|
var import_node_os3 = require("node:os");
|
|
7245
|
-
var
|
|
7741
|
+
var import_node_path4 = require("node:path");
|
|
7246
7742
|
function getRuntimeConfig(program2, manifestOverride) {
|
|
7247
7743
|
const options = program2.opts();
|
|
7248
7744
|
const threadnoteHome = expandPath(options.home ?? process.env.THREADNOTE_HOME ?? "~/.openviking");
|
|
@@ -7261,20 +7757,20 @@ function getRuntimeConfig(program2, manifestOverride) {
|
|
|
7261
7757
|
};
|
|
7262
7758
|
}
|
|
7263
7759
|
function defaultManifestPath(agentContextHome) {
|
|
7264
|
-
const userManifest = (0,
|
|
7760
|
+
const userManifest = (0, import_node_path4.join)(agentContextHome, USER_MANIFEST_NAME);
|
|
7265
7761
|
return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
|
|
7266
7762
|
}
|
|
7267
7763
|
function builtInExampleManifestPath() {
|
|
7268
|
-
return (0,
|
|
7764
|
+
return (0, import_node_path4.join)(toolRoot(), "config", "seed-manifest.example.yaml");
|
|
7269
7765
|
}
|
|
7270
7766
|
function openVikingHealthUrl(config) {
|
|
7271
7767
|
return `http://${config.host}:${config.port}/health`;
|
|
7272
7768
|
}
|
|
7273
7769
|
function openVikingLogPath(config) {
|
|
7274
|
-
return (0,
|
|
7770
|
+
return (0, import_node_path4.join)(config.agentContextHome, "logs", "server.log");
|
|
7275
7771
|
}
|
|
7276
7772
|
function openVikingServerArgs(config) {
|
|
7277
|
-
return ["--config", (0,
|
|
7773
|
+
return ["--config", (0, import_node_path4.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
|
|
7278
7774
|
}
|
|
7279
7775
|
function withIdentity(config, args) {
|
|
7280
7776
|
return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
|
|
@@ -7290,7 +7786,7 @@ function renderTemplate(template, config, extras = {}) {
|
|
|
7290
7786
|
// src/share.ts
|
|
7291
7787
|
var import_promises4 = require("node:fs/promises");
|
|
7292
7788
|
var import_node_os4 = require("node:os");
|
|
7293
|
-
var
|
|
7789
|
+
var import_node_path5 = require("node:path");
|
|
7294
7790
|
var TEAMS_FILE_VERSION = 1;
|
|
7295
7791
|
var SHARED_SEGMENT = "shared";
|
|
7296
7792
|
var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
|
|
@@ -7359,8 +7855,8 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
7359
7855
|
if (await exists(gitdir)) {
|
|
7360
7856
|
throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
|
|
7361
7857
|
}
|
|
7362
|
-
await ensureDirectory((0,
|
|
7363
|
-
await ensureDirectory((0,
|
|
7858
|
+
await ensureDirectory((0, import_node_path5.dirname)(worktree), dryRun);
|
|
7859
|
+
await ensureDirectory((0, import_node_path5.dirname)(gitdir), dryRun);
|
|
7364
7860
|
const git = await requiredExecutable("git");
|
|
7365
7861
|
await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
|
|
7366
7862
|
const newConfig = {
|
|
@@ -7391,7 +7887,7 @@ async function runShareInit(config, remoteUrl, options) {
|
|
|
7391
7887
|
var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
|
|
7392
7888
|
var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
|
|
7393
7889
|
async function ensureSharedGitignore(worktree, git, push) {
|
|
7394
|
-
const gitignorePath = (0,
|
|
7890
|
+
const gitignorePath = (0, import_node_path5.join)(worktree, ".gitignore");
|
|
7395
7891
|
const existing = await readFileIfExists(gitignorePath) ?? "";
|
|
7396
7892
|
const lines = existing.split("\n").map((line) => line.trim());
|
|
7397
7893
|
const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
|
|
@@ -7492,7 +7988,7 @@ function autoShareState(config) {
|
|
|
7492
7988
|
return state;
|
|
7493
7989
|
}
|
|
7494
7990
|
function pendingReindexesPath(config) {
|
|
7495
|
-
return (0,
|
|
7991
|
+
return (0, import_node_path5.join)(config.agentContextHome, "share", "auto-sync-pending-reindexes.json");
|
|
7496
7992
|
}
|
|
7497
7993
|
async function loadPendingReindexes(config, state) {
|
|
7498
7994
|
const raw = await readFileIfExists(pendingReindexesPath(config));
|
|
@@ -7536,7 +8032,7 @@ async function writePendingReindexes(config, state) {
|
|
|
7536
8032
|
teams: Object.fromEntries(state.pendingReindexes),
|
|
7537
8033
|
version: 1
|
|
7538
8034
|
};
|
|
7539
|
-
await (0, import_promises4.mkdir)((0,
|
|
8035
|
+
await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
|
|
7540
8036
|
const tempPath = `${path}.${process.pid}.tmp`;
|
|
7541
8037
|
await (0, import_promises4.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
|
|
7542
8038
|
`, { encoding: "utf8", mode: 384 });
|
|
@@ -7629,7 +8125,7 @@ async function runShareSync(config, options) {
|
|
|
7629
8125
|
if (dryRun) {
|
|
7630
8126
|
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
|
|
7631
8127
|
} else if (pullResult && pullResult.exitCode !== 0) {
|
|
7632
|
-
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"))) {
|
|
7633
8129
|
throw new Error(
|
|
7634
8130
|
`git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
|
|
7635
8131
|
Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
|
|
@@ -7684,7 +8180,7 @@ async function runShareSyncQuiet(config, state, options) {
|
|
|
7684
8180
|
const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
|
|
7685
8181
|
const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
|
|
7686
8182
|
if (pullResult.exitCode !== 0) {
|
|
7687
|
-
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"))) {
|
|
7688
8184
|
throw new Error(
|
|
7689
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.`
|
|
7690
8186
|
);
|
|
@@ -7858,10 +8354,10 @@ function normalizeTeamName(input2) {
|
|
|
7858
8354
|
return candidate;
|
|
7859
8355
|
}
|
|
7860
8356
|
function teamsFilePath(config) {
|
|
7861
|
-
return (0,
|
|
8357
|
+
return (0, import_node_path5.join)(config.agentContextHome, "share", "teams.json");
|
|
7862
8358
|
}
|
|
7863
8359
|
function teamWorktreePath(config, team) {
|
|
7864
|
-
return (0,
|
|
8360
|
+
return (0, import_node_path5.join)(
|
|
7865
8361
|
config.agentContextHome,
|
|
7866
8362
|
"data",
|
|
7867
8363
|
"viking",
|
|
@@ -7874,7 +8370,7 @@ function teamWorktreePath(config, team) {
|
|
|
7874
8370
|
);
|
|
7875
8371
|
}
|
|
7876
8372
|
function teamGitdirPath(config, team) {
|
|
7877
|
-
return (0,
|
|
8373
|
+
return (0, import_node_path5.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
|
|
7878
8374
|
}
|
|
7879
8375
|
async function readTeamsFile(config) {
|
|
7880
8376
|
const path = teamsFilePath(config);
|
|
@@ -7917,7 +8413,7 @@ async function readTeamsFile(config) {
|
|
|
7917
8413
|
}
|
|
7918
8414
|
async function writeTeamsFile(config, contents) {
|
|
7919
8415
|
const path = teamsFilePath(config);
|
|
7920
|
-
await (0, import_promises4.mkdir)((0,
|
|
8416
|
+
await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
|
|
7921
8417
|
const serializable = {
|
|
7922
8418
|
defaultTeam: contents.defaultTeam,
|
|
7923
8419
|
teams: contents.teams,
|
|
@@ -7986,7 +8482,7 @@ async function walkMemoryFiles(root) {
|
|
|
7986
8482
|
if (entry.name === ".git") {
|
|
7987
8483
|
continue;
|
|
7988
8484
|
}
|
|
7989
|
-
const full = (0,
|
|
8485
|
+
const full = (0, import_node_path5.join)(path, entry.name);
|
|
7990
8486
|
if (entry.isDirectory()) {
|
|
7991
8487
|
if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
|
|
7992
8488
|
continue;
|
|
@@ -8010,7 +8506,7 @@ async function walkMemoryFiles(root) {
|
|
|
8010
8506
|
return out;
|
|
8011
8507
|
}
|
|
8012
8508
|
function workfileToVikingUri(config, team, filePath) {
|
|
8013
|
-
const rel = (0,
|
|
8509
|
+
const rel = (0, import_node_path5.relative)(team.worktree, filePath).split(import_node_path5.sep).join("/");
|
|
8014
8510
|
return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
|
|
8015
8511
|
}
|
|
8016
8512
|
function isInSharedNamespace(config, uri) {
|
|
@@ -8130,8 +8626,8 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun, op
|
|
|
8130
8626
|
}
|
|
8131
8627
|
return;
|
|
8132
8628
|
}
|
|
8133
|
-
const stagingDir = await (0, import_promises4.mkdtemp)((0,
|
|
8134
|
-
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");
|
|
8135
8631
|
try {
|
|
8136
8632
|
await (0, import_promises4.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
|
|
8137
8633
|
await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode, options);
|
|
@@ -8253,7 +8749,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
|
8253
8749
|
if (!relative3) {
|
|
8254
8750
|
return;
|
|
8255
8751
|
}
|
|
8256
|
-
await (0, import_promises4.rm)((0,
|
|
8752
|
+
await (0, import_promises4.rm)((0, import_node_path5.join)(worktree, relative3), { force: true });
|
|
8257
8753
|
}
|
|
8258
8754
|
async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
|
|
8259
8755
|
const args = withIdentity(config, ["rm", uri]);
|
|
@@ -8317,8 +8813,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
8317
8813
|
const oldRel = entries[index + 1];
|
|
8318
8814
|
const newRel = entries[index + 2];
|
|
8319
8815
|
if (oldRel && newRel) {
|
|
8320
|
-
changes.push({ path: (0,
|
|
8321
|
-
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" });
|
|
8322
8818
|
}
|
|
8323
8819
|
index += 3;
|
|
8324
8820
|
continue;
|
|
@@ -8326,7 +8822,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
|
|
|
8326
8822
|
const rel = entries[index + 1];
|
|
8327
8823
|
if (rel) {
|
|
8328
8824
|
const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
|
|
8329
|
-
changes.push({ path: (0,
|
|
8825
|
+
changes.push({ path: (0, import_node_path5.join)(worktree, rel), relativePath: rel, status });
|
|
8330
8826
|
}
|
|
8331
8827
|
index += 2;
|
|
8332
8828
|
}
|
|
@@ -8410,6 +8906,12 @@ function parseMemoryStatus(value) {
|
|
|
8410
8906
|
}
|
|
8411
8907
|
throw new Error(`Unsupported memory status "${value}". Expected active, archived, or superseded.`);
|
|
8412
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
|
+
}
|
|
8413
8915
|
async function runRemember(config, options) {
|
|
8414
8916
|
const text = await getInputText(options.text, options.stdin === true);
|
|
8415
8917
|
if (!text.trim()) {
|
|
@@ -8417,11 +8919,11 @@ async function runRemember(config, options) {
|
|
|
8417
8919
|
}
|
|
8418
8920
|
const metadata = {
|
|
8419
8921
|
kind: options.kind ?? "durable",
|
|
8420
|
-
project:
|
|
8922
|
+
project: normalizeOptionalMetadata2(options.project),
|
|
8421
8923
|
sourceAgentClient: options.sourceAgentClient ?? "codex",
|
|
8422
8924
|
status: options.status ?? "active",
|
|
8423
8925
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8424
|
-
topic:
|
|
8926
|
+
topic: normalizeOptionalMetadata2(options.topic)
|
|
8425
8927
|
};
|
|
8426
8928
|
await storeMemory(config, {
|
|
8427
8929
|
bodyText: text.trim(),
|
|
@@ -8442,7 +8944,7 @@ async function runMigrateMemories(config, options) {
|
|
|
8442
8944
|
const candidates = await legacyMemoryCandidates(config, sourceAccounts);
|
|
8443
8945
|
const existingHashes = await existingDurableMemoryHashes(config);
|
|
8444
8946
|
const ov = await openVikingCliForMode(dryRun);
|
|
8445
|
-
const migrationPath = (0,
|
|
8947
|
+
const migrationPath = (0, import_node_path6.join)(config.agentContextHome, "legacy-memory-migration.txt");
|
|
8446
8948
|
let duplicateCount = 0;
|
|
8447
8949
|
let migratedCount = 0;
|
|
8448
8950
|
let sensitiveCount = 0;
|
|
@@ -8505,7 +9007,7 @@ async function runMigrateLifecycle(config, options) {
|
|
|
8505
9007
|
const limit = options.limit ? parsePositiveInteger(options.limit, "lifecycle migration limit") : void 0;
|
|
8506
9008
|
const ov = await openVikingCliForMode(dryRun);
|
|
8507
9009
|
const candidates = await legacyLifecycleHandoffCandidates(config);
|
|
8508
|
-
const migrationPath = (0,
|
|
9010
|
+
const migrationPath = (0, import_node_path6.join)(config.agentContextHome, "lifecycle-memory-migration.txt");
|
|
8509
9011
|
let existingCount = 0;
|
|
8510
9012
|
let migratedCount = 0;
|
|
8511
9013
|
let skippedCount = 0;
|
|
@@ -8515,7 +9017,7 @@ async function runMigrateLifecycle(config, options) {
|
|
|
8515
9017
|
break;
|
|
8516
9018
|
}
|
|
8517
9019
|
const destinationUri = lifecycleMigrationUri(config, candidate.metadata, sha256(candidate.original.trim()));
|
|
8518
|
-
const migratedMemory =
|
|
9020
|
+
const migratedMemory = formatMemoryDocument2(
|
|
8519
9021
|
"HANDOFF",
|
|
8520
9022
|
candidate.metadata,
|
|
8521
9023
|
["Migrated legacy handoff from the historical events trail.", "", candidate.original.trim()].join("\n")
|
|
@@ -8561,8 +9063,10 @@ async function runRecall(config, options) {
|
|
|
8561
9063
|
await syncSharedReposAndLog(config);
|
|
8562
9064
|
}
|
|
8563
9065
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8564
|
-
const
|
|
8565
|
-
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];
|
|
8566
9070
|
if (inferredUri) {
|
|
8567
9071
|
args.push("--uri", inferredUri);
|
|
8568
9072
|
console.log(`Recall scope: ${inferredUri}`);
|
|
@@ -8570,24 +9074,41 @@ async function runRecall(config, options) {
|
|
|
8570
9074
|
if (options.nodeLimit) {
|
|
8571
9075
|
args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
|
|
8572
9076
|
}
|
|
8573
|
-
|
|
8574
|
-
await
|
|
8575
|
-
|
|
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, {
|
|
8576
9093
|
dryRun: options.dryRun === true,
|
|
8577
9094
|
includeArchived: options.includeArchived === true
|
|
8578
9095
|
});
|
|
9096
|
+
if (exactOutput) {
|
|
9097
|
+
recallOutputs.push(exactOutput);
|
|
9098
|
+
}
|
|
9099
|
+
await printRecallHygieneNudges(config, recallOutputs.join("\n"));
|
|
8579
9100
|
}
|
|
8580
|
-
async function augmentRecallWithSeededResources(config, ov, options, inferredUri) {
|
|
9101
|
+
async function augmentRecallWithSeededResources(config, ov, options, inferredUri, projectQuery = options.query) {
|
|
8581
9102
|
if (options.uri || options.inferScope === false) {
|
|
8582
|
-
return;
|
|
9103
|
+
return void 0;
|
|
8583
9104
|
}
|
|
8584
|
-
const project = await inferProjectFromQuery(config.manifestPath,
|
|
9105
|
+
const project = await inferProjectFromQuery(config.manifestPath, projectQuery);
|
|
8585
9106
|
if (!project) {
|
|
8586
|
-
return;
|
|
9107
|
+
return void 0;
|
|
8587
9108
|
}
|
|
8588
9109
|
const projectResourceUri = trimTrailingSlash(project.uri);
|
|
8589
9110
|
if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
|
|
8590
|
-
return;
|
|
9111
|
+
return void 0;
|
|
8591
9112
|
}
|
|
8592
9113
|
const args = ["search", options.query, "--uri", projectResourceUri];
|
|
8593
9114
|
if (options.nodeLimit) {
|
|
@@ -8595,7 +9116,8 @@ async function augmentRecallWithSeededResources(config, ov, options, inferredUri
|
|
|
8595
9116
|
}
|
|
8596
9117
|
console.log(`
|
|
8597
9118
|
Also searching seeded resources: ${projectResourceUri}`);
|
|
8598
|
-
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;
|
|
8599
9121
|
}
|
|
8600
9122
|
async function runRead(config, uri, options) {
|
|
8601
9123
|
assertVikingUri(uri);
|
|
@@ -8623,6 +9145,147 @@ async function syncSharedReposAndLog(config) {
|
|
|
8623
9145
|
console.error(`Auto-sync warning: ${err instanceof Error ? err.message : String(err)}`);
|
|
8624
9146
|
}
|
|
8625
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
|
+
}
|
|
8626
9289
|
async function runList(config, uri, options) {
|
|
8627
9290
|
assertVikingUri(uri);
|
|
8628
9291
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
@@ -8660,11 +9323,11 @@ async function runArchive(config, uri, options) {
|
|
|
8660
9323
|
const fallbackMetadata = {
|
|
8661
9324
|
archivedFrom: uri,
|
|
8662
9325
|
kind: options.kind ?? "handoff",
|
|
8663
|
-
project:
|
|
9326
|
+
project: normalizeOptionalMetadata2(options.project),
|
|
8664
9327
|
sourceAgentClient: "threadnote",
|
|
8665
9328
|
status: "archived",
|
|
8666
9329
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8667
|
-
topic:
|
|
9330
|
+
topic: normalizeOptionalMetadata2(options.topic)
|
|
8668
9331
|
};
|
|
8669
9332
|
await storeMemory(config, {
|
|
8670
9333
|
bodyText: ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n"),
|
|
@@ -8682,11 +9345,11 @@ async function runArchive(config, uri, options) {
|
|
|
8682
9345
|
const metadata = {
|
|
8683
9346
|
archivedFrom: uri,
|
|
8684
9347
|
kind: options.kind ?? inferredMetadata.kind ?? "handoff",
|
|
8685
|
-
project:
|
|
9348
|
+
project: normalizeOptionalMetadata2(options.project) ?? inferredMetadata.project,
|
|
8686
9349
|
sourceAgentClient: "threadnote",
|
|
8687
9350
|
status: "archived",
|
|
8688
9351
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8689
|
-
topic:
|
|
9352
|
+
topic: normalizeOptionalMetadata2(options.topic) ?? inferredMetadata.topic
|
|
8690
9353
|
};
|
|
8691
9354
|
await storeMemory(config, {
|
|
8692
9355
|
bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
|
|
@@ -8715,7 +9378,7 @@ async function runForget(config, uri, options) {
|
|
|
8715
9378
|
}
|
|
8716
9379
|
async function runExportPack(config, options) {
|
|
8717
9380
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
8718
|
-
const defaultPath = (0,
|
|
9381
|
+
const defaultPath = (0, import_node_path6.join)(config.agentContextHome, `threadnote-${safeTimestamp()}.ovpack`);
|
|
8719
9382
|
const outputPath = expandPath(options.path ?? defaultPath);
|
|
8720
9383
|
await maybeRun(options.dryRun === true, ov, withIdentity(config, ["export", options.uri ?? "viking://", outputPath]));
|
|
8721
9384
|
}
|
|
@@ -8740,7 +9403,7 @@ async function inferRecallUri(config, query) {
|
|
|
8740
9403
|
async function printExactMemoryMatches(config, ov, query, options) {
|
|
8741
9404
|
const terms = exactRecallTerms(query);
|
|
8742
9405
|
if (terms.length === 0) {
|
|
8743
|
-
return;
|
|
9406
|
+
return void 0;
|
|
8744
9407
|
}
|
|
8745
9408
|
const scopes = exactMemoryScopes(config, options.includeArchived);
|
|
8746
9409
|
const outputs = [];
|
|
@@ -8752,30 +9415,32 @@ async function printExactMemoryMatches(config, ov, query, options) {
|
|
|
8752
9415
|
continue;
|
|
8753
9416
|
}
|
|
8754
9417
|
const result = await runCommand(ov, args, { allowFailure: true });
|
|
8755
|
-
const
|
|
8756
|
-
if (result.exitCode === 0 && grepOutputHasMatches(
|
|
8757
|
-
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);
|
|
8758
9421
|
}
|
|
8759
9422
|
}
|
|
8760
9423
|
}
|
|
8761
9424
|
if (outputs.length === 0) {
|
|
8762
|
-
return;
|
|
9425
|
+
return void 0;
|
|
8763
9426
|
}
|
|
8764
|
-
console.log("\nExact
|
|
8765
|
-
|
|
9427
|
+
console.log("\nExact memory/resource matches:");
|
|
9428
|
+
const output2 = outputs.join("\n\n");
|
|
9429
|
+
console.log(output2);
|
|
9430
|
+
return output2;
|
|
8766
9431
|
}
|
|
8767
9432
|
async function storeMemory(config, options) {
|
|
8768
9433
|
if (options.replaceUri) {
|
|
8769
9434
|
assertVikingUri(options.replaceUri);
|
|
8770
9435
|
}
|
|
8771
9436
|
const ov = await openVikingCliForMode(options.dryRun);
|
|
8772
|
-
const memoryPath = (0,
|
|
9437
|
+
const memoryPath = (0, import_node_path6.join)(config.agentContextHome, "last-memory.txt");
|
|
8773
9438
|
const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
|
|
8774
|
-
const candidateMemory =
|
|
9439
|
+
const candidateMemory = formatMemoryDocument2(options.title, candidateMetadata, options.bodyText);
|
|
8775
9440
|
const memoryUri = memoryUriFor(config, candidateMemory, candidateMetadata);
|
|
8776
9441
|
const isInPlaceUpdate = options.replaceUri !== void 0 && options.replaceUri === memoryUri;
|
|
8777
9442
|
const finalMetadata = isInPlaceUpdate ? { ...options.metadata, supersedes: void 0 } : candidateMetadata;
|
|
8778
|
-
const memory = isInPlaceUpdate ?
|
|
9443
|
+
const memory = isInPlaceUpdate ? formatMemoryDocument2(options.title, finalMetadata, options.bodyText) : candidateMemory;
|
|
8779
9444
|
const writeMode = await memoryWriteMode(ov, config, memoryUri, finalMetadata);
|
|
8780
9445
|
if (options.dryRun) {
|
|
8781
9446
|
console.log(memory);
|
|
@@ -8917,7 +9582,7 @@ async function hasLegacyLifecycleHandoffCandidates(config) {
|
|
|
8917
9582
|
return (await legacyLifecycleHandoffCandidates(config, 1)).length > 0;
|
|
8918
9583
|
}
|
|
8919
9584
|
async function legacyLifecycleHandoffCandidates(config, limit) {
|
|
8920
|
-
const eventsRoot = (0,
|
|
9585
|
+
const eventsRoot = (0, import_node_path6.join)(localUserMemoriesRoot(config), "events");
|
|
8921
9586
|
let entries;
|
|
8922
9587
|
try {
|
|
8923
9588
|
entries = await (0, import_promises5.readdir)(eventsRoot, { withFileTypes: true });
|
|
@@ -8929,7 +9594,7 @@ async function legacyLifecycleHandoffCandidates(config, limit) {
|
|
|
8929
9594
|
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
|
|
8930
9595
|
continue;
|
|
8931
9596
|
}
|
|
8932
|
-
const sourcePath = (0,
|
|
9597
|
+
const sourcePath = (0, import_node_path6.join)(eventsRoot, entry.name);
|
|
8933
9598
|
const original = await readTextIfExists(sourcePath);
|
|
8934
9599
|
if (!original || !isClearLegacyHandoffMemory(original) || sensitiveMemoryReason(original)) {
|
|
8935
9600
|
continue;
|
|
@@ -9015,7 +9680,7 @@ function vikingDirectoryChain(directoryUri) {
|
|
|
9015
9680
|
}
|
|
9016
9681
|
return chain;
|
|
9017
9682
|
}
|
|
9018
|
-
function
|
|
9683
|
+
function formatMemoryDocument2(title, metadata, body) {
|
|
9019
9684
|
const header = [
|
|
9020
9685
|
title,
|
|
9021
9686
|
`kind: ${metadata.kind}`,
|
|
@@ -9032,10 +9697,10 @@ function formatMemoryDocument(title, metadata, body) {
|
|
|
9032
9697
|
function inferMemoryMetadata(memory) {
|
|
9033
9698
|
const header = memory.slice(0, Math.max(0, memory.indexOf("\n\n")) || memory.length);
|
|
9034
9699
|
const firstLine2 = header.split("\n")[0]?.trim();
|
|
9035
|
-
const kind =
|
|
9036
|
-
const status =
|
|
9037
|
-
const project =
|
|
9038
|
-
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"));
|
|
9039
9704
|
return {
|
|
9040
9705
|
kind,
|
|
9041
9706
|
project,
|
|
@@ -9071,9 +9736,9 @@ function inferLegacyProject(memory) {
|
|
|
9071
9736
|
return "general";
|
|
9072
9737
|
}
|
|
9073
9738
|
const trimmed = explicit.trim().replace(/[`.,;]+$/g, "");
|
|
9074
|
-
return trimmed.includes("/") ? (0,
|
|
9739
|
+
return trimmed.includes("/") ? (0, import_node_path6.basename)(trimmed) : trimmed;
|
|
9075
9740
|
}
|
|
9076
|
-
function
|
|
9741
|
+
function parseOptionalMemoryKind2(value) {
|
|
9077
9742
|
if (!value) {
|
|
9078
9743
|
return void 0;
|
|
9079
9744
|
}
|
|
@@ -9083,7 +9748,7 @@ function parseOptionalMemoryKind(value) {
|
|
|
9083
9748
|
return void 0;
|
|
9084
9749
|
}
|
|
9085
9750
|
}
|
|
9086
|
-
function
|
|
9751
|
+
function parseOptionalMemoryStatus2(value) {
|
|
9087
9752
|
if (!value) {
|
|
9088
9753
|
return void 0;
|
|
9089
9754
|
}
|
|
@@ -9093,7 +9758,7 @@ function parseOptionalMemoryStatus(value) {
|
|
|
9093
9758
|
return void 0;
|
|
9094
9759
|
}
|
|
9095
9760
|
}
|
|
9096
|
-
function
|
|
9761
|
+
function normalizeOptionalMetadata2(value) {
|
|
9097
9762
|
const trimmed = value?.trim();
|
|
9098
9763
|
return trimmed ? trimmed : void 0;
|
|
9099
9764
|
}
|
|
@@ -9111,14 +9776,14 @@ async function legacySourceAccounts(config, options) {
|
|
|
9111
9776
|
async function legacyMemoryCandidates(config, sourceAccounts) {
|
|
9112
9777
|
const candidates = [];
|
|
9113
9778
|
for (const sourceAccount of sourceAccounts) {
|
|
9114
|
-
const sessionRoot = (0,
|
|
9779
|
+
const sessionRoot = (0, import_node_path6.join)(localVikingDataRoot(config), sourceAccount, "session");
|
|
9115
9780
|
for (const sourceSession of await childDirectoryNames(sessionRoot)) {
|
|
9116
|
-
const historyRoot = (0,
|
|
9781
|
+
const historyRoot = (0, import_node_path6.join)(sessionRoot, sourceSession, "history");
|
|
9117
9782
|
for (const sourceArchive of await childDirectoryNames(historyRoot)) {
|
|
9118
9783
|
if (!sourceArchive.startsWith("archive_")) {
|
|
9119
9784
|
continue;
|
|
9120
9785
|
}
|
|
9121
|
-
const sourcePath = (0,
|
|
9786
|
+
const sourcePath = (0, import_node_path6.join)(historyRoot, sourceArchive, "messages.jsonl");
|
|
9122
9787
|
for (const text of await legacyMemoryTexts(sourcePath)) {
|
|
9123
9788
|
candidates.push({
|
|
9124
9789
|
comparableHash: sha256(comparableMemoryText(text)),
|
|
@@ -9186,7 +9851,7 @@ async function collectDurableMemoryHashes(root, hashes) {
|
|
|
9186
9851
|
return;
|
|
9187
9852
|
}
|
|
9188
9853
|
for (const entry of entries) {
|
|
9189
|
-
const path = (0,
|
|
9854
|
+
const path = (0, import_node_path6.join)(root, entry.name);
|
|
9190
9855
|
if (entry.isDirectory()) {
|
|
9191
9856
|
await collectDurableMemoryHashes(path, hashes);
|
|
9192
9857
|
continue;
|
|
@@ -9203,7 +9868,7 @@ async function collectDurableMemoryHashes(root, hashes) {
|
|
|
9203
9868
|
}
|
|
9204
9869
|
}
|
|
9205
9870
|
function isDurableMemoryPath(path) {
|
|
9206
|
-
return path.split(
|
|
9871
|
+
return path.split(import_node_path6.sep).includes("memories");
|
|
9207
9872
|
}
|
|
9208
9873
|
async function childDirectoryNames(path) {
|
|
9209
9874
|
let entries;
|
|
@@ -9247,10 +9912,10 @@ function legacySourceLabel(candidate) {
|
|
|
9247
9912
|
return `${candidate.sourceAccount}/${candidate.sourceSession}/${candidate.sourceArchive}`;
|
|
9248
9913
|
}
|
|
9249
9914
|
function localVikingDataRoot(config) {
|
|
9250
|
-
return (0,
|
|
9915
|
+
return (0, import_node_path6.join)(config.agentContextHome, "data", "viking");
|
|
9251
9916
|
}
|
|
9252
9917
|
function localUserMemoriesRoot(config) {
|
|
9253
|
-
return (0,
|
|
9918
|
+
return (0, import_node_path6.join)(localVikingDataRoot(config), config.account, "user", uriSegment(config.user), "memories");
|
|
9254
9919
|
}
|
|
9255
9920
|
function uniqueStrings(values) {
|
|
9256
9921
|
return [...new Set(values)].sort();
|
|
@@ -9266,14 +9931,15 @@ async function buildHandoff(options) {
|
|
|
9266
9931
|
const status = await gitValue(["status", "--short"], repoRoot) ?? "";
|
|
9267
9932
|
const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
|
|
9268
9933
|
const touchedFiles = await gitTouchedFiles(repoRoot);
|
|
9269
|
-
const repoName = (0,
|
|
9934
|
+
const repoName = (0, import_node_path6.basename)(repoRoot);
|
|
9935
|
+
const topicBranch = branch && branch !== "unknown" ? branch : "current";
|
|
9270
9936
|
const metadata = {
|
|
9271
9937
|
kind: "handoff",
|
|
9272
|
-
project:
|
|
9938
|
+
project: normalizeOptionalMetadata2(options.project) ?? repoName,
|
|
9273
9939
|
sourceAgentClient: options.sourceAgentClient ?? "codex",
|
|
9274
9940
|
status: "active",
|
|
9275
9941
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9276
|
-
topic:
|
|
9942
|
+
topic: handoffTopicForBranch(topicBranch, { timestamped: options.timestamped, topic: options.topic })
|
|
9277
9943
|
};
|
|
9278
9944
|
const bodyText = [
|
|
9279
9945
|
`repo: ${repoName}`,
|
|
@@ -9326,7 +9992,7 @@ function formatBlock(value, emptyValue) {
|
|
|
9326
9992
|
// src/update-check.ts
|
|
9327
9993
|
var import_node_child_process2 = require("node:child_process");
|
|
9328
9994
|
var import_promises6 = require("node:fs/promises");
|
|
9329
|
-
var
|
|
9995
|
+
var import_node_path7 = require("node:path");
|
|
9330
9996
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
9331
9997
|
var NPM_LATEST_URL = "https://registry.npmjs.org/threadnote/latest";
|
|
9332
9998
|
var FETCH_TIMEOUT_MS = 3e3;
|
|
@@ -9405,7 +10071,7 @@ async function readUpdateCache(cachePath) {
|
|
|
9405
10071
|
}
|
|
9406
10072
|
async function writeUpdateCache(cachePath, contents) {
|
|
9407
10073
|
try {
|
|
9408
|
-
await (0, import_promises6.mkdir)((0,
|
|
10074
|
+
await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(cachePath), { recursive: true });
|
|
9409
10075
|
await (0, import_promises6.writeFile)(cachePath, `${JSON.stringify(contents)}
|
|
9410
10076
|
`, { encoding: "utf8", mode: 384 });
|
|
9411
10077
|
} catch {
|
|
@@ -9429,14 +10095,14 @@ async function fetchLatestVersion() {
|
|
|
9429
10095
|
|
|
9430
10096
|
// src/version.ts
|
|
9431
10097
|
var import_node_fs4 = require("node:fs");
|
|
9432
|
-
var
|
|
10098
|
+
var import_node_path8 = require("node:path");
|
|
9433
10099
|
var cachedVersion;
|
|
9434
10100
|
function getThreadnoteVersion() {
|
|
9435
10101
|
if (cachedVersion !== void 0) {
|
|
9436
10102
|
return cachedVersion;
|
|
9437
10103
|
}
|
|
9438
10104
|
try {
|
|
9439
|
-
const packageJsonPath = (0,
|
|
10105
|
+
const packageJsonPath = (0, import_node_path8.join)(__dirname, "..", "package.json");
|
|
9440
10106
|
const parsed = JSON.parse((0, import_node_fs4.readFileSync)(packageJsonPath, "utf8"));
|
|
9441
10107
|
cachedVersion = typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : "unknown";
|
|
9442
10108
|
} catch {
|
|
@@ -9495,7 +10161,7 @@ async function runClaudeHooksInstall(options) {
|
|
|
9495
10161
|
console.log("\nRe-run with --apply to actually modify the file.");
|
|
9496
10162
|
return;
|
|
9497
10163
|
}
|
|
9498
|
-
await (0, import_promises7.mkdir)((0,
|
|
10164
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path), { recursive: true });
|
|
9499
10165
|
const serialized = `${JSON.stringify(next, void 0, 2)}
|
|
9500
10166
|
`;
|
|
9501
10167
|
await (0, import_promises7.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
|
|
@@ -9595,7 +10261,7 @@ async function hasManagedClaudeHooks() {
|
|
|
9595
10261
|
async function runPreCompactHook(config, options = {}) {
|
|
9596
10262
|
try {
|
|
9597
10263
|
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
|
|
9598
|
-
const project = repoRoot ? (0,
|
|
10264
|
+
const project = repoRoot ? (0, import_node_path9.basename)(repoRoot) : "general";
|
|
9599
10265
|
await runHandoff(config, {
|
|
9600
10266
|
blockers: "- none recorded",
|
|
9601
10267
|
dryRun: options.dryRun === true,
|
|
@@ -9619,7 +10285,7 @@ async function runSessionStartHook(config, options = {}) {
|
|
|
9619
10285
|
if (!repoRoot) {
|
|
9620
10286
|
return;
|
|
9621
10287
|
}
|
|
9622
|
-
const project = (0,
|
|
10288
|
+
const project = (0, import_node_path9.basename)(repoRoot);
|
|
9623
10289
|
await emitUpdateBannerIfOutdated(config);
|
|
9624
10290
|
process.stdout.write(`## Threadnote \u2014 latest context for ${project}
|
|
9625
10291
|
|
|
@@ -9628,7 +10294,8 @@ async function runSessionStartHook(config, options = {}) {
|
|
|
9628
10294
|
dryRun: options.dryRun === true,
|
|
9629
10295
|
inferScope: true,
|
|
9630
10296
|
nodeLimit: "5",
|
|
9631
|
-
|
|
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`
|
|
9632
10299
|
});
|
|
9633
10300
|
} catch (err) {
|
|
9634
10301
|
process.stderr.write(
|
|
@@ -9640,7 +10307,7 @@ async function runSessionStartHook(config, options = {}) {
|
|
|
9640
10307
|
async function emitUpdateBannerIfOutdated(config) {
|
|
9641
10308
|
try {
|
|
9642
10309
|
const result = await checkForThreadnoteUpdate({
|
|
9643
|
-
cachePath: (0,
|
|
10310
|
+
cachePath: (0, import_node_path9.join)(config.agentContextHome, ".update-state.json"),
|
|
9644
10311
|
currentVersion: getThreadnoteVersion()
|
|
9645
10312
|
});
|
|
9646
10313
|
if (!result || !result.outdated) {
|
|
@@ -9666,12 +10333,12 @@ async function emitUpdateBannerIfOutdated(config) {
|
|
|
9666
10333
|
|
|
9667
10334
|
// src/seeding.ts
|
|
9668
10335
|
var import_promises8 = require("node:fs/promises");
|
|
9669
|
-
var
|
|
10336
|
+
var import_node_path10 = require("node:path");
|
|
9670
10337
|
async function runSeed(config, options) {
|
|
9671
10338
|
const manifest = await readSeedManifest(config.manifestPath);
|
|
9672
10339
|
const ignorePatterns = await loadIgnorePatterns();
|
|
9673
10340
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
9674
|
-
const statePath = (0,
|
|
10341
|
+
const statePath = (0, import_node_path10.join)(config.agentContextHome, SEED_STATE_FILE);
|
|
9675
10342
|
const state = options.force === true ? { files: {}, version: 1 } : await readSeedState(statePath);
|
|
9676
10343
|
const projects = filterProjects(manifest.projects, options.only);
|
|
9677
10344
|
let importedCount = 0;
|
|
@@ -9751,13 +10418,13 @@ async function readSeedState(path) {
|
|
|
9751
10418
|
}
|
|
9752
10419
|
}
|
|
9753
10420
|
async function writeSeedState(path, state) {
|
|
9754
|
-
await ensureDirectory((0,
|
|
10421
|
+
await ensureDirectory((0, import_node_path10.dirname)(path), false);
|
|
9755
10422
|
await (0, import_promises8.writeFile)(path, `${JSON.stringify(state, void 0, 2)}
|
|
9756
10423
|
`, { encoding: "utf8", mode: 384 });
|
|
9757
10424
|
}
|
|
9758
10425
|
async function runInitManifest(config, options) {
|
|
9759
10426
|
const manifestPath = expandPath(
|
|
9760
|
-
options.path ?? process.env.THREADNOTE_MANIFEST ?? (0,
|
|
10427
|
+
options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path10.join)(config.agentContextHome, USER_MANIFEST_NAME)
|
|
9761
10428
|
);
|
|
9762
10429
|
const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
|
|
9763
10430
|
const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
|
|
@@ -9800,7 +10467,7 @@ async function runInitManifest(config, options) {
|
|
|
9800
10467
|
console.log(output2.trimEnd());
|
|
9801
10468
|
return;
|
|
9802
10469
|
}
|
|
9803
|
-
await ensureDirectory((0,
|
|
10470
|
+
await ensureDirectory((0, import_node_path10.dirname)(manifestPath), false);
|
|
9804
10471
|
await (0, import_promises8.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
|
|
9805
10472
|
await (0, import_promises8.chmod)(manifestPath, 384);
|
|
9806
10473
|
console.log(`Wrote manifest: ${manifestPath}`);
|
|
@@ -9838,7 +10505,7 @@ async function projectIdentity(path) {
|
|
|
9838
10505
|
}
|
|
9839
10506
|
}
|
|
9840
10507
|
function projectManifestForRepo(repoRoot, existingProjects) {
|
|
9841
|
-
const baseName = uriSegment((0,
|
|
10508
|
+
const baseName = uriSegment((0, import_node_path10.basename)(repoRoot));
|
|
9842
10509
|
const usedNames = new Set(existingProjects.map((project) => project.name));
|
|
9843
10510
|
const usedUris = new Set(existingProjects.map((project) => project.uri));
|
|
9844
10511
|
let name = baseName;
|
|
@@ -9860,7 +10527,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
|
|
|
9860
10527
|
for (const pattern of project.seed) {
|
|
9861
10528
|
const files = await resolveProjectPattern(projectRoot, pattern);
|
|
9862
10529
|
for (const filePath of files) {
|
|
9863
|
-
const relativePath = toPosixPath((0,
|
|
10530
|
+
const relativePath = toPosixPath((0, import_node_path10.relative)(projectRoot, filePath));
|
|
9864
10531
|
if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
|
|
9865
10532
|
continue;
|
|
9866
10533
|
}
|
|
@@ -9878,17 +10545,17 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
|
|
|
9878
10545
|
async function resolveProjectPattern(projectRoot, pattern) {
|
|
9879
10546
|
const normalizedPattern = toPosixPath(pattern);
|
|
9880
10547
|
if (!hasGlob(normalizedPattern)) {
|
|
9881
|
-
const filePath = (0,
|
|
10548
|
+
const filePath = (0, import_node_path10.join)(projectRoot, normalizedPattern);
|
|
9882
10549
|
return await isFile(filePath) ? [filePath] : [];
|
|
9883
10550
|
}
|
|
9884
10551
|
const globBase = getGlobBase(normalizedPattern);
|
|
9885
|
-
const basePath = (0,
|
|
10552
|
+
const basePath = (0, import_node_path10.join)(projectRoot, globBase);
|
|
9886
10553
|
if (!await exists(basePath)) {
|
|
9887
10554
|
return [];
|
|
9888
10555
|
}
|
|
9889
10556
|
const regex = globToRegExp(normalizedPattern);
|
|
9890
10557
|
const files = await walkFiles(basePath);
|
|
9891
|
-
return files.filter((filePath) => regex.test(toPosixPath((0,
|
|
10558
|
+
return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path10.relative)(projectRoot, filePath))));
|
|
9892
10559
|
}
|
|
9893
10560
|
async function prepareSeedFile(config, candidate, dryRun) {
|
|
9894
10561
|
const content = await (0, import_promises8.readFile)(candidate.filePath, "utf8");
|
|
@@ -9903,12 +10570,12 @@ async function prepareSeedFile(config, candidate, dryRun) {
|
|
|
9903
10570
|
if (redactedContent === content) {
|
|
9904
10571
|
return candidate.filePath;
|
|
9905
10572
|
}
|
|
9906
|
-
const redactedPath = (0,
|
|
10573
|
+
const redactedPath = (0, import_node_path10.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
|
|
9907
10574
|
if (dryRun) {
|
|
9908
10575
|
console.log(`Would write redacted copy: ${redactedPath}`);
|
|
9909
10576
|
return redactedPath;
|
|
9910
10577
|
}
|
|
9911
|
-
await ensureDirectory((0,
|
|
10578
|
+
await ensureDirectory((0, import_node_path10.dirname)(redactedPath), false);
|
|
9912
10579
|
await (0, import_promises8.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
|
|
9913
10580
|
await (0, import_promises8.chmod)(redactedPath, 384);
|
|
9914
10581
|
return redactedPath;
|
|
@@ -9966,10 +10633,10 @@ async function resolveAbsolutePattern(pattern) {
|
|
|
9966
10633
|
return files.filter((filePath) => regex.test(toPosixPath(filePath)));
|
|
9967
10634
|
}
|
|
9968
10635
|
function skillResourceUri(skill) {
|
|
9969
|
-
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`;
|
|
9970
10637
|
}
|
|
9971
10638
|
async function loadIgnorePatterns() {
|
|
9972
|
-
const raw = await (0, import_promises8.readFile)((0,
|
|
10639
|
+
const raw = await (0, import_promises8.readFile)((0, import_node_path10.join)(toolRoot(), ".threadnoteignore"), "utf8");
|
|
9973
10640
|
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
9974
10641
|
}
|
|
9975
10642
|
function matchesIgnore(relativePath, patterns) {
|
|
@@ -10044,7 +10711,7 @@ var import_node_child_process3 = require("node:child_process");
|
|
|
10044
10711
|
var import_node_fs6 = require("node:fs");
|
|
10045
10712
|
var import_promises11 = require("node:fs/promises");
|
|
10046
10713
|
var import_node_os6 = require("node:os");
|
|
10047
|
-
var
|
|
10714
|
+
var import_node_path12 = require("node:path");
|
|
10048
10715
|
var import_node_process2 = require("node:process");
|
|
10049
10716
|
var import_promises12 = require("node:readline/promises");
|
|
10050
10717
|
|
|
@@ -10052,7 +10719,7 @@ var import_promises12 = require("node:readline/promises");
|
|
|
10052
10719
|
var import_node_fs5 = require("node:fs");
|
|
10053
10720
|
var import_promises9 = require("node:fs/promises");
|
|
10054
10721
|
var import_node_os5 = require("node:os");
|
|
10055
|
-
var
|
|
10722
|
+
var import_node_path11 = require("node:path");
|
|
10056
10723
|
var import_promises10 = require("node:readline/promises");
|
|
10057
10724
|
var import_node_process = require("node:process");
|
|
10058
10725
|
var NPM_PACKAGE_NAME = "threadnote";
|
|
@@ -10267,7 +10934,7 @@ async function waitForOpenVikingHealthy(config, timeoutMs) {
|
|
|
10267
10934
|
return isOpenVikingHealthy(config);
|
|
10268
10935
|
}
|
|
10269
10936
|
function launchAgentPlistPath() {
|
|
10270
|
-
return (0,
|
|
10937
|
+
return (0, import_node_path11.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
|
|
10271
10938
|
}
|
|
10272
10939
|
async function isLaunchAgentInstalled() {
|
|
10273
10940
|
if (process.platform !== "darwin") {
|
|
@@ -10303,7 +10970,7 @@ async function getUpdateInfo(config, options) {
|
|
|
10303
10970
|
};
|
|
10304
10971
|
}
|
|
10305
10972
|
async function currentPackageVersion() {
|
|
10306
|
-
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");
|
|
10307
10974
|
const parsed = JSON.parse(rawPackage);
|
|
10308
10975
|
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
10309
10976
|
throw new Error("Could not read current threadnote package version.");
|
|
@@ -10357,7 +11024,7 @@ async function writeUpdateCache2(config, cache) {
|
|
|
10357
11024
|
`, { encoding: "utf8", mode: 384 });
|
|
10358
11025
|
}
|
|
10359
11026
|
function updateCachePath(config) {
|
|
10360
|
-
return (0,
|
|
11027
|
+
return (0, import_node_path11.join)(config.agentContextHome, "update-check.json");
|
|
10361
11028
|
}
|
|
10362
11029
|
async function runApplicablePostUpdateMigrations(config, options) {
|
|
10363
11030
|
const state = await readPostUpdateState(config);
|
|
@@ -10421,7 +11088,7 @@ async function applicablePostUpdateMigrations(config, options) {
|
|
|
10421
11088
|
return applicable;
|
|
10422
11089
|
}
|
|
10423
11090
|
async function readPostUpdateMigrations() {
|
|
10424
|
-
const raw = await readFileIfExists((0,
|
|
11091
|
+
const raw = await readFileIfExists((0, import_node_path11.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
|
|
10425
11092
|
if (!raw) {
|
|
10426
11093
|
return [];
|
|
10427
11094
|
}
|
|
@@ -10499,7 +11166,7 @@ async function writePostUpdateState(config, state) {
|
|
|
10499
11166
|
`, { encoding: "utf8", mode: 384 });
|
|
10500
11167
|
}
|
|
10501
11168
|
function postUpdateStatePath(config) {
|
|
10502
|
-
return (0,
|
|
11169
|
+
return (0, import_node_path11.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
|
|
10503
11170
|
}
|
|
10504
11171
|
async function resolveUpdateRuntime(runtime) {
|
|
10505
11172
|
if (runtime !== "auto") {
|
|
@@ -10529,14 +11196,14 @@ async function runtimeThreadnoteBin(runtime) {
|
|
|
10529
11196
|
if (runtime === "npm") {
|
|
10530
11197
|
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
10531
11198
|
const prefix = result.stdout.trim();
|
|
10532
|
-
return prefix ? (0,
|
|
11199
|
+
return prefix ? (0, import_node_path11.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
10533
11200
|
}
|
|
10534
11201
|
if (runtime === "bun") {
|
|
10535
11202
|
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
10536
11203
|
const binDir = result.stdout.trim();
|
|
10537
|
-
return binDir ? (0,
|
|
11204
|
+
return binDir ? (0, import_node_path11.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
10538
11205
|
}
|
|
10539
|
-
return (0,
|
|
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);
|
|
10540
11207
|
}
|
|
10541
11208
|
function updatePackageCommand(runtime, registry) {
|
|
10542
11209
|
if (runtime === "npm") {
|
|
@@ -10591,9 +11258,9 @@ async function runDoctor(config, options) {
|
|
|
10591
11258
|
checks.push(await commandShimCheck());
|
|
10592
11259
|
checks.push(...await userAgentInstructionsChecks());
|
|
10593
11260
|
checks.push(await manifestCheck(config.manifestPath));
|
|
10594
|
-
checks.push(await fileCheck((0,
|
|
10595
|
-
checks.push(await fileCheck((0,
|
|
10596
|
-
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"));
|
|
10597
11264
|
checks.push(await healthCheck(config));
|
|
10598
11265
|
for (const check of checks) {
|
|
10599
11266
|
console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
|
|
@@ -10610,9 +11277,9 @@ async function runInstall(config, options) {
|
|
|
10610
11277
|
const repairInvalidConfigs = options.repairInvalidConfigs === true;
|
|
10611
11278
|
const dryRun = options.dryRun === true;
|
|
10612
11279
|
await ensureDirectory(config.agentContextHome, dryRun);
|
|
10613
|
-
await ensureDirectory((0,
|
|
10614
|
-
await ensureDirectory((0,
|
|
10615
|
-
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);
|
|
10616
11283
|
await installCommandShim(dryRun);
|
|
10617
11284
|
await installUserAgentInstructions(dryRun);
|
|
10618
11285
|
const serverPath = await findOpenVikingServer();
|
|
@@ -10649,17 +11316,17 @@ async function runInstall(config, options) {
|
|
|
10649
11316
|
}
|
|
10650
11317
|
await writeTemplateIfMissing({
|
|
10651
11318
|
config,
|
|
10652
|
-
destinationPath: (0,
|
|
11319
|
+
destinationPath: (0, import_node_path12.join)(config.agentContextHome, "ov.conf"),
|
|
10653
11320
|
dryRun,
|
|
10654
11321
|
shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
10655
|
-
templatePath: (0,
|
|
11322
|
+
templatePath: (0, import_node_path12.join)(toolRoot(), "config", "ov.conf.template.json")
|
|
10656
11323
|
});
|
|
10657
11324
|
await writeTemplateIfMissing({
|
|
10658
11325
|
config,
|
|
10659
|
-
destinationPath: (0,
|
|
11326
|
+
destinationPath: (0, import_node_path12.join)(config.agentContextHome, "ovcli.conf"),
|
|
10660
11327
|
dryRun,
|
|
10661
11328
|
shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
|
|
10662
|
-
templatePath: (0,
|
|
11329
|
+
templatePath: (0, import_node_path12.join)(toolRoot(), "config", "ovcli.conf.template.json")
|
|
10663
11330
|
});
|
|
10664
11331
|
if (options.start !== false) {
|
|
10665
11332
|
const healthy = await repairServerHealth(config, dryRun);
|
|
@@ -10713,7 +11380,7 @@ async function runUninstall(config, options) {
|
|
|
10713
11380
|
}
|
|
10714
11381
|
console.log("Uninstalling local Threadnote setup.");
|
|
10715
11382
|
await runStop(config, { dryRun });
|
|
10716
|
-
await removePathIfExists((0,
|
|
11383
|
+
await removePathIfExists((0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
|
|
10717
11384
|
await removeLaunchAgent(dryRun);
|
|
10718
11385
|
await removeMcpConfigs(options.mcp ?? "available", dryRun);
|
|
10719
11386
|
await removeMcpSnippets(config, dryRun);
|
|
@@ -10770,7 +11437,7 @@ async function repairManifest(config, dryRun) {
|
|
|
10770
11437
|
console.log(output2.trimEnd());
|
|
10771
11438
|
return;
|
|
10772
11439
|
}
|
|
10773
|
-
await ensureDirectory((0,
|
|
11440
|
+
await ensureDirectory((0, import_node_path12.dirname)(config.manifestPath), false);
|
|
10774
11441
|
const currentContent = await readFileIfExists(config.manifestPath);
|
|
10775
11442
|
if (currentContent !== void 0) {
|
|
10776
11443
|
const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
|
|
@@ -10788,7 +11455,7 @@ async function findOpenVikingServer() {
|
|
|
10788
11455
|
return onPath;
|
|
10789
11456
|
}
|
|
10790
11457
|
for (const candidateDir of await openVikingServerCandidateDirs()) {
|
|
10791
|
-
const candidate = (0,
|
|
11458
|
+
const candidate = (0, import_node_path12.join)(candidateDir, OPENVIKING_SERVER_COMMAND);
|
|
10792
11459
|
if (await isExecutable(candidate)) {
|
|
10793
11460
|
return candidate;
|
|
10794
11461
|
}
|
|
@@ -10834,7 +11501,7 @@ async function maybePrintOpenVikingPathHint(serverPath) {
|
|
|
10834
11501
|
if (onPath) {
|
|
10835
11502
|
return;
|
|
10836
11503
|
}
|
|
10837
|
-
const binDir = (0,
|
|
11504
|
+
const binDir = (0, import_node_path12.dirname)(serverPath);
|
|
10838
11505
|
const rcHint = suggestedShellRc(process.env.SHELL, (0, import_node_os6.platform)());
|
|
10839
11506
|
console.log(
|
|
10840
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.`
|
|
@@ -10878,7 +11545,7 @@ async function runStart(config, options) {
|
|
|
10878
11545
|
);
|
|
10879
11546
|
}
|
|
10880
11547
|
const logPath = openVikingLogPath(config);
|
|
10881
|
-
await ensureDirectory((0,
|
|
11548
|
+
await ensureDirectory((0, import_node_path12.dirname)(logPath), false);
|
|
10882
11549
|
if (options.foreground === true) {
|
|
10883
11550
|
const result = await runInteractive(server, args);
|
|
10884
11551
|
process.exitCode = result;
|
|
@@ -10887,7 +11554,7 @@ async function runStart(config, options) {
|
|
|
10887
11554
|
const logFd = (0, import_node_fs6.openSync)(logPath, "a");
|
|
10888
11555
|
const child = spawnDetachedServerWithLog(server, args, logFd);
|
|
10889
11556
|
child.unref();
|
|
10890
|
-
await (0, import_promises11.writeFile)((0,
|
|
11557
|
+
await (0, import_promises11.writeFile)((0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
|
|
10891
11558
|
`, "utf8");
|
|
10892
11559
|
const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
|
|
10893
11560
|
if (health) {
|
|
@@ -10920,7 +11587,7 @@ async function runStop(config, options) {
|
|
|
10920
11587
|
console.log(`No LaunchAgent found: ${launchAgentPath}`);
|
|
10921
11588
|
}
|
|
10922
11589
|
}
|
|
10923
|
-
const pidPath = (0,
|
|
11590
|
+
const pidPath = (0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid");
|
|
10924
11591
|
const pidText = await readFileIfExists(pidPath);
|
|
10925
11592
|
if (!pidText) {
|
|
10926
11593
|
console.log("No pid file found for detached OpenViking server.");
|
|
@@ -10962,7 +11629,7 @@ async function openVikingServerCheck() {
|
|
|
10962
11629
|
}
|
|
10963
11630
|
const result = await runCommand(executable, ["--help"], { allowFailure: true });
|
|
10964
11631
|
const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
|
|
10965
|
-
const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0,
|
|
11632
|
+
const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path12.dirname)(executable)} to PATH)`;
|
|
10966
11633
|
return {
|
|
10967
11634
|
name,
|
|
10968
11635
|
status: result.exitCode === 0 ? "ok" : "warn",
|
|
@@ -11031,7 +11698,7 @@ async function pythonSystemCertificatesCheck() {
|
|
|
11031
11698
|
};
|
|
11032
11699
|
}
|
|
11033
11700
|
async function commandShimCheck() {
|
|
11034
|
-
const shimPath = (0,
|
|
11701
|
+
const shimPath = (0, import_node_path12.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
11035
11702
|
const content = await readFileIfExists(shimPath);
|
|
11036
11703
|
if (content === void 0) {
|
|
11037
11704
|
return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
|
|
@@ -11101,7 +11768,7 @@ async function siblingPythonForExecutable(executablePath) {
|
|
|
11101
11768
|
} catch (_err) {
|
|
11102
11769
|
return void 0;
|
|
11103
11770
|
}
|
|
11104
|
-
const pythonPath = (0,
|
|
11771
|
+
const pythonPath = (0, import_node_path12.join)((0, import_node_path12.dirname)(resolvedPath), "python");
|
|
11105
11772
|
return await exists(pythonPath) ? pythonPath : void 0;
|
|
11106
11773
|
}
|
|
11107
11774
|
async function manifestCheck(path) {
|
|
@@ -11321,14 +11988,14 @@ async function writeTemplateIfMissing(options) {
|
|
|
11321
11988
|
console.log(`Would write ${options.destinationPath}`);
|
|
11322
11989
|
return;
|
|
11323
11990
|
}
|
|
11324
|
-
await ensureDirectory((0,
|
|
11991
|
+
await ensureDirectory((0, import_node_path12.dirname)(options.destinationPath), false);
|
|
11325
11992
|
await (0, import_promises11.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
|
|
11326
11993
|
await (0, import_promises11.chmod)(options.destinationPath, 384);
|
|
11327
11994
|
console.log(`Wrote ${options.destinationPath}`);
|
|
11328
11995
|
}
|
|
11329
11996
|
async function installCommandShim(dryRun) {
|
|
11330
11997
|
const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
|
|
11331
|
-
const shimPath = (0,
|
|
11998
|
+
const shimPath = (0, import_node_path12.join)(binDir, "threadnote");
|
|
11332
11999
|
const existingContent = await readFileIfExists(shimPath);
|
|
11333
12000
|
if (existingContent && !isManagedCommandShim(existingContent)) {
|
|
11334
12001
|
console.log(`WARN not overwriting existing command shim: ${shimPath}`);
|
|
@@ -11349,7 +12016,7 @@ async function installCommandShim(dryRun) {
|
|
|
11349
12016
|
console.log(`Wrote command shim: ${shimPath}`);
|
|
11350
12017
|
}
|
|
11351
12018
|
async function removeCommandShim(dryRun) {
|
|
11352
|
-
const shimPath = (0,
|
|
12019
|
+
const shimPath = (0, import_node_path12.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
|
|
11353
12020
|
const content = await readFileIfExists(shimPath);
|
|
11354
12021
|
if (content === void 0) {
|
|
11355
12022
|
console.log(`Already absent: ${shimPath}`);
|
|
@@ -11383,7 +12050,7 @@ async function installUserAgentInstructions(dryRun) {
|
|
|
11383
12050
|
console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
|
|
11384
12051
|
continue;
|
|
11385
12052
|
}
|
|
11386
|
-
await ensureDirectory((0,
|
|
12053
|
+
await ensureDirectory((0, import_node_path12.dirname)(targetPath), false);
|
|
11387
12054
|
await (0, import_promises11.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
|
|
11388
12055
|
console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
|
|
11389
12056
|
}
|
|
@@ -11441,7 +12108,7 @@ async function renderUserAgentInstructions(target) {
|
|
|
11441
12108
|
].join("\n");
|
|
11442
12109
|
}
|
|
11443
12110
|
async function renderUserAgentInstructionsBlock() {
|
|
11444
|
-
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();
|
|
11445
12112
|
return `${USER_INSTRUCTIONS_START_MARKER}
|
|
11446
12113
|
${instructions}
|
|
11447
12114
|
${USER_INSTRUCTIONS_END_MARKER}`;
|
|
@@ -11525,7 +12192,7 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
11525
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.`
|
|
11526
12193
|
);
|
|
11527
12194
|
}
|
|
11528
|
-
const source = (0,
|
|
12195
|
+
const source = (0, import_node_path12.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
|
|
11529
12196
|
const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
|
|
11530
12197
|
const rendered = renderTemplate(await (0, import_promises11.readFile)(source, "utf8"), config, {
|
|
11531
12198
|
OPENVIKING_SERVER_PATH: resolvedServer ?? OPENVIKING_SERVER_COMMAND
|
|
@@ -11539,8 +12206,8 @@ async function installLaunchAgent(config, dryRun) {
|
|
|
11539
12206
|
console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
|
|
11540
12207
|
return;
|
|
11541
12208
|
}
|
|
11542
|
-
await ensureDirectory((0,
|
|
11543
|
-
await ensureDirectory((0,
|
|
12209
|
+
await ensureDirectory((0, import_node_path12.dirname)(destination), false);
|
|
12210
|
+
await ensureDirectory((0, import_node_path12.dirname)(openVikingLogPath(config)), false);
|
|
11544
12211
|
await (0, import_promises11.writeFile)(destination, rendered, "utf8");
|
|
11545
12212
|
await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
|
|
11546
12213
|
await maybeRun(false, "launchctl", ["load", destination]);
|
|
@@ -11604,7 +12271,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
|
|
|
11604
12271
|
if (typeof parsed.default_user !== "string") {
|
|
11605
12272
|
return false;
|
|
11606
12273
|
}
|
|
11607
|
-
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0,
|
|
12274
|
+
if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path12.join)(config.agentContextHome, "data")) {
|
|
11608
12275
|
return false;
|
|
11609
12276
|
}
|
|
11610
12277
|
return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
|
|
@@ -11724,16 +12391,19 @@ async function main() {
|
|
|
11724
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) => {
|
|
11725
12392
|
await runMigrateLifecycle(getRuntimeConfig(program2), options);
|
|
11726
12393
|
});
|
|
11727
|
-
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) => {
|
|
11728
12395
|
await runRecall(getRuntimeConfig(program2), options);
|
|
11729
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
|
+
});
|
|
11730
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) => {
|
|
11731
12401
|
await runRead(getRuntimeConfig(program2), uri, options);
|
|
11732
12402
|
});
|
|
11733
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) => {
|
|
11734
12404
|
await runList(getRuntimeConfig(program2), uri, options);
|
|
11735
12405
|
});
|
|
11736
|
-
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) => {
|
|
11737
12407
|
await runHandoff(getRuntimeConfig(program2), options);
|
|
11738
12408
|
});
|
|
11739
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) => {
|