unforgit 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +233 -12
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command31 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import fs2 from "fs";
|
|
@@ -3108,14 +3108,111 @@ Examples:
|
|
|
3108
3108
|
}
|
|
3109
3109
|
});
|
|
3110
3110
|
|
|
3111
|
-
// src/commands/
|
|
3111
|
+
// src/commands/dashboard.ts
|
|
3112
|
+
import { spawn as spawn2 } from "child_process";
|
|
3113
|
+
import fs5 from "fs";
|
|
3114
|
+
import path4 from "path";
|
|
3115
|
+
import { fileURLToPath } from "url";
|
|
3112
3116
|
import { Command as Command26 } from "commander";
|
|
3117
|
+
function assertSafeDashboardBind(host, allowNetwork = false) {
|
|
3118
|
+
if ((host === "0.0.0.0" || host === "::") && !allowNetwork) {
|
|
3119
|
+
throw new Error(
|
|
3120
|
+
`Refusing to bind dashboard to ${host} without --allow-network. Use a specific Tailscale/LAN IP when possible.`
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
function parseDashboardPort(port) {
|
|
3125
|
+
const value = port === void 0 ? 3838 : Number(port);
|
|
3126
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
3127
|
+
throw new Error("--port must be between 1 and 65535");
|
|
3128
|
+
}
|
|
3129
|
+
return value;
|
|
3130
|
+
}
|
|
3131
|
+
function buildDashboardLaunchOptions(input) {
|
|
3132
|
+
const host = input.host ?? "127.0.0.1";
|
|
3133
|
+
assertSafeDashboardBind(host, input.allowNetwork ?? false);
|
|
3134
|
+
const port = parseDashboardPort(input.port);
|
|
3135
|
+
const workspace = path4.resolve(input.workspace ?? input.cwd);
|
|
3136
|
+
return {
|
|
3137
|
+
host,
|
|
3138
|
+
port,
|
|
3139
|
+
workspace,
|
|
3140
|
+
url: `http://${host}:${port}`
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
function resolveDashboardAppDir(moduleUrl = import.meta.url) {
|
|
3144
|
+
if (process.env.UNFORGIT_DASHBOARD_APP_DIR) {
|
|
3145
|
+
return path4.resolve(process.env.UNFORGIT_DASHBOARD_APP_DIR);
|
|
3146
|
+
}
|
|
3147
|
+
const thisFile = fileURLToPath(moduleUrl);
|
|
3148
|
+
const dir = path4.dirname(thisFile);
|
|
3149
|
+
if (path4.basename(dir) === "dist") {
|
|
3150
|
+
return path4.resolve(dir, "../../web");
|
|
3151
|
+
}
|
|
3152
|
+
return path4.resolve(dir, "../../../web");
|
|
3153
|
+
}
|
|
3154
|
+
var dashboardCommand = new Command26("dashboard").description("Start the local Unforgit memory dashboard").option("--host <host>", "Host/IP to bind (default: 127.0.0.1)").option("--port <port>", "Port to listen on (default: 3838)").option("--workspace <path>", "Workspace containing .unforgit/ (default: current directory)").option(
|
|
3155
|
+
"--allow-network",
|
|
3156
|
+
"Allow wildcard binds such as 0.0.0.0. Prefer a specific Tailscale/LAN IP instead."
|
|
3157
|
+
).addHelpText("after", `
|
|
3158
|
+
Examples:
|
|
3159
|
+
unforgit dashboard
|
|
3160
|
+
unforgit dashboard --port 4848
|
|
3161
|
+
unforgit dashboard --workspace ~/.hermes/unforgit-memory --host 100.81.12.32`).action((opts) => {
|
|
3162
|
+
let launch;
|
|
3163
|
+
try {
|
|
3164
|
+
launch = buildDashboardLaunchOptions({
|
|
3165
|
+
cwd: process.cwd(),
|
|
3166
|
+
host: opts.host,
|
|
3167
|
+
port: opts.port,
|
|
3168
|
+
workspace: opts.workspace,
|
|
3169
|
+
allowNetwork: opts.allowNetwork
|
|
3170
|
+
});
|
|
3171
|
+
} catch (err) {
|
|
3172
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
3173
|
+
process.exit(EXIT_ERROR);
|
|
3174
|
+
}
|
|
3175
|
+
const dashboardAppDir = resolveDashboardAppDir();
|
|
3176
|
+
if (!fs5.existsSync(path4.join(dashboardAppDir, "package.json"))) {
|
|
3177
|
+
logger.error(`Dashboard app not found at ${dashboardAppDir}.`);
|
|
3178
|
+
logger.error("Set UNFORGIT_DASHBOARD_APP_DIR to the apps/web directory.");
|
|
3179
|
+
process.exit(EXIT_ERROR);
|
|
3180
|
+
}
|
|
3181
|
+
logger.info(`Starting Unforgit dashboard: ${launch.url}`);
|
|
3182
|
+
logger.info(`Workspace: ${launch.workspace}`);
|
|
3183
|
+
if (launch.host !== "127.0.0.1" && launch.host !== "localhost") {
|
|
3184
|
+
logger.warn("Dashboard is bound to a network address. Use only on trusted private networks such as Tailscale.");
|
|
3185
|
+
}
|
|
3186
|
+
const child = spawn2(
|
|
3187
|
+
"pnpm",
|
|
3188
|
+
["exec", "next", "dev", "-p", String(launch.port), "-H", launch.host],
|
|
3189
|
+
{
|
|
3190
|
+
cwd: dashboardAppDir,
|
|
3191
|
+
stdio: "inherit",
|
|
3192
|
+
env: {
|
|
3193
|
+
...process.env,
|
|
3194
|
+
UNFORGIT_WORKSPACE: launch.workspace
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
);
|
|
3198
|
+
child.on("error", (err) => {
|
|
3199
|
+
logger.error(`Failed to start dashboard: ${err.message}`);
|
|
3200
|
+
process.exit(EXIT_ERROR);
|
|
3201
|
+
});
|
|
3202
|
+
child.on("exit", (code, signal) => {
|
|
3203
|
+
if (signal) process.kill(process.pid, signal);
|
|
3204
|
+
process.exit(code ?? 0);
|
|
3205
|
+
});
|
|
3206
|
+
});
|
|
3207
|
+
|
|
3208
|
+
// src/commands/doctor.ts
|
|
3209
|
+
import { Command as Command27 } from "commander";
|
|
3113
3210
|
import { loadConfig as loadConfig22, getDbPath as getDbPath20, isInitialized as isInitialized16, getConfigPath as getConfigPath2 } from "unforgit-config";
|
|
3114
3211
|
import { LocalStore as LocalStore19 } from "unforgit-db";
|
|
3115
3212
|
import { RemoteClient as RemoteClient13 } from "unforgit-config";
|
|
3116
|
-
import
|
|
3213
|
+
import fs6 from "fs";
|
|
3117
3214
|
import YAML from "yaml";
|
|
3118
|
-
var doctorCommand = new
|
|
3215
|
+
var doctorCommand = new Command27("doctor").description("Check system health and diagnose common issues").action(async () => {
|
|
3119
3216
|
const results = [];
|
|
3120
3217
|
if (!isInitialized16()) {
|
|
3121
3218
|
results.push({
|
|
@@ -3149,7 +3246,7 @@ var doctorCommand = new Command26("doctor").description("Check system health and
|
|
|
3149
3246
|
}
|
|
3150
3247
|
if (process.platform !== "win32") {
|
|
3151
3248
|
try {
|
|
3152
|
-
const stat =
|
|
3249
|
+
const stat = fs6.statSync(configPath);
|
|
3153
3250
|
const mode = stat.mode & 511;
|
|
3154
3251
|
if (mode & 36) {
|
|
3155
3252
|
results.push({
|
|
@@ -3325,7 +3422,7 @@ function exitForResults(results) {
|
|
|
3325
3422
|
}
|
|
3326
3423
|
function findDeprecatedConfigKeys(configPath) {
|
|
3327
3424
|
try {
|
|
3328
|
-
const raw =
|
|
3425
|
+
const raw = fs6.readFileSync(configPath, "utf-8");
|
|
3329
3426
|
const parsed = YAML.parse(raw) ?? {};
|
|
3330
3427
|
const deprecated = [];
|
|
3331
3428
|
if (parsed.remote?.apiKey) {
|
|
@@ -3361,14 +3458,14 @@ function printResults(results) {
|
|
|
3361
3458
|
}
|
|
3362
3459
|
|
|
3363
3460
|
// src/commands/completion.ts
|
|
3364
|
-
import { Command as
|
|
3461
|
+
import { Command as Command28 } from "commander";
|
|
3365
3462
|
var BASH_COMPLETION = `
|
|
3366
3463
|
_unforgit_completions() {
|
|
3367
3464
|
local cur prev commands
|
|
3368
3465
|
COMPREPLY=()
|
|
3369
3466
|
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
3370
3467
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
3371
|
-
commands="init add recall promote consolidate deprecate supersede delete restore web link unlink links merge remerge similar history auto-consolidate unconsolidate status push pull remote log diff keys auth config embeddings reset doctor curate completion"
|
|
3468
|
+
commands="init add recall promote consolidate deprecate supersede delete restore web dashboard link unlink links merge remerge similar history auto-consolidate unconsolidate status push pull remote log diff keys auth config embeddings reset doctor curate completion"
|
|
3372
3469
|
|
|
3373
3470
|
case "\${prev}" in
|
|
3374
3471
|
unforgit)
|
|
@@ -3424,6 +3521,7 @@ _unforgit() {
|
|
|
3424
3521
|
'delete:Soft delete a memory'
|
|
3425
3522
|
'restore:Restore a soft-deleted memory'
|
|
3426
3523
|
'web:Start the web dashboard'
|
|
3524
|
+
'dashboard:Start the local memory dashboard'
|
|
3427
3525
|
'link:Create a link between memories'
|
|
3428
3526
|
'unlink:Remove a link between memories'
|
|
3429
3527
|
'links:List links for a memory'
|
|
@@ -3479,6 +3577,7 @@ complete -c unforgit -n '__fish_use_subcommand' -a 'supersede' -d 'Supersede a m
|
|
|
3479
3577
|
complete -c unforgit -n '__fish_use_subcommand' -a 'delete' -d 'Delete a memory'
|
|
3480
3578
|
complete -c unforgit -n '__fish_use_subcommand' -a 'restore' -d 'Restore a memory'
|
|
3481
3579
|
complete -c unforgit -n '__fish_use_subcommand' -a 'web' -d 'Start web dashboard'
|
|
3580
|
+
complete -c unforgit -n '__fish_use_subcommand' -a 'dashboard' -d 'Start local memory dashboard'
|
|
3482
3581
|
complete -c unforgit -n '__fish_use_subcommand' -a 'link' -d 'Link two memories'
|
|
3483
3582
|
complete -c unforgit -n '__fish_use_subcommand' -a 'unlink' -d 'Unlink memories'
|
|
3484
3583
|
complete -c unforgit -n '__fish_use_subcommand' -a 'links' -d 'List links'
|
|
@@ -3506,7 +3605,7 @@ complete -c unforgit -l verbose -d 'Enable verbose output'
|
|
|
3506
3605
|
complete -c unforgit -l quiet -d 'Suppress non-essential output'
|
|
3507
3606
|
complete -c unforgit -l json -d 'Output as JSON'
|
|
3508
3607
|
`.trim();
|
|
3509
|
-
var completionCommand = new
|
|
3608
|
+
var completionCommand = new Command28("completion").description("Generate shell completion scripts").argument("<shell>", "Shell type (bash, zsh, fish)").addHelpText("after", `
|
|
3510
3609
|
Examples:
|
|
3511
3610
|
unforgit completion bash >> ~/.bashrc
|
|
3512
3611
|
unforgit completion zsh >> ~/.zshrc
|
|
@@ -3528,7 +3627,7 @@ Examples:
|
|
|
3528
3627
|
});
|
|
3529
3628
|
|
|
3530
3629
|
// src/commands/curate.ts
|
|
3531
|
-
import { Command as
|
|
3630
|
+
import { Command as Command29 } from "commander";
|
|
3532
3631
|
import { getDbPath as getDbPath21, isInitialized as isInitialized17, loadConfig as loadConfig23 } from "unforgit-config";
|
|
3533
3632
|
import { LocalStore as LocalStore20 } from "unforgit-db";
|
|
3534
3633
|
import { RemoteClient as RemoteClient14 } from "unforgit-config";
|
|
@@ -3545,7 +3644,7 @@ function formatCandidatePreview2(candidate) {
|
|
|
3545
3644
|
}
|
|
3546
3645
|
return lines.join("\n");
|
|
3547
3646
|
}
|
|
3548
|
-
var curateCommand = new
|
|
3647
|
+
var curateCommand = new Command29("curate").description("Preview or run lifecycle maintenance for repository memories").option("--remote", "Run maintenance against the remote server").option("--execute", "Apply changes instead of previewing them").option("--model <model>", "OpenAI model to use for consolidation").option("--no-preserve", "Do not preserve original memories during consolidation").addHelpText("after", `
|
|
3549
3648
|
Examples:
|
|
3550
3649
|
unforgit curate
|
|
3551
3650
|
unforgit curate --execute
|
|
@@ -3641,6 +3740,126 @@ async function runRemoteCurate(remoteUrl, options) {
|
|
|
3641
3740
|
return client.runLifecycle(options);
|
|
3642
3741
|
}
|
|
3643
3742
|
|
|
3743
|
+
// src/commands/suggestions.ts
|
|
3744
|
+
import { Command as Command30 } from "commander";
|
|
3745
|
+
import { getDbPath as getDbPath22, isInitialized as isInitialized18, loadConfig as loadConfig24 } from "unforgit-config";
|
|
3746
|
+
import { LocalStore as LocalStore21 } from "unforgit-db";
|
|
3747
|
+
import { generateSuggestions, persistReviewableSuggestions } from "unforgit-core";
|
|
3748
|
+
function openStore() {
|
|
3749
|
+
if (!isInitialized18()) {
|
|
3750
|
+
logger.error("Unforgit not initialized. Run 'unforgit init' first.");
|
|
3751
|
+
process.exit(EXIT_CONFIG_ERROR);
|
|
3752
|
+
}
|
|
3753
|
+
const config = loadConfig24();
|
|
3754
|
+
return {
|
|
3755
|
+
config,
|
|
3756
|
+
orgId: config.remote.orgId || "local",
|
|
3757
|
+
repoId: config.remote.repoId || "local",
|
|
3758
|
+
store: new LocalStore21(getDbPath22())
|
|
3759
|
+
};
|
|
3760
|
+
}
|
|
3761
|
+
function formatSuggestionLine(suggestion) {
|
|
3762
|
+
const memoryIds = suggestion.memoryIds.map((id) => id.slice(0, 8)).join(", ");
|
|
3763
|
+
return [
|
|
3764
|
+
`${suggestion.id.slice(0, 8)} [${suggestion.priority}] ${suggestion.type}`,
|
|
3765
|
+
` status: ${suggestion.status}`,
|
|
3766
|
+
` confidence: ${Math.round(suggestion.confidence * 100)}%`,
|
|
3767
|
+
` memories: ${memoryIds || "none"}`,
|
|
3768
|
+
` reason: ${suggestion.reason}`
|
|
3769
|
+
].join("\n");
|
|
3770
|
+
}
|
|
3771
|
+
function parseStatus(value) {
|
|
3772
|
+
if (!value) return ["pending"];
|
|
3773
|
+
const statuses = value.split(",").map((status) => status.trim()).filter(Boolean);
|
|
3774
|
+
const allowed = /* @__PURE__ */ new Set(["pending", "approved", "rejected", "applied"]);
|
|
3775
|
+
const invalid = statuses.find((status) => !allowed.has(status));
|
|
3776
|
+
if (invalid) {
|
|
3777
|
+
throw new Error(`Invalid suggestion status: ${invalid}`);
|
|
3778
|
+
}
|
|
3779
|
+
return statuses;
|
|
3780
|
+
}
|
|
3781
|
+
var suggestionsCommand = new Command30("suggestions").description("Reviewable curation suggestions inbox").addCommand(
|
|
3782
|
+
new Command30("generate").description("Generate and persist pending review suggestions").option("--max <n>", "Maximum generated suggestions", (value) => Number.parseInt(value, 10), 20).option("--created-by <name>", "Actor name stored on generated suggestions", "cli").action((opts) => {
|
|
3783
|
+
const { store, orgId, repoId } = openStore();
|
|
3784
|
+
try {
|
|
3785
|
+
const generated = generateSuggestions(store, orgId, repoId, {
|
|
3786
|
+
maxSuggestions: opts.max
|
|
3787
|
+
});
|
|
3788
|
+
const persisted = persistReviewableSuggestions(
|
|
3789
|
+
store,
|
|
3790
|
+
orgId,
|
|
3791
|
+
repoId,
|
|
3792
|
+
generated.suggestions,
|
|
3793
|
+
{ createdBy: opts.createdBy }
|
|
3794
|
+
);
|
|
3795
|
+
const result = { ...generated.stats, ...persisted };
|
|
3796
|
+
if (isJsonMode()) {
|
|
3797
|
+
outputJson(result);
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
logger.info(
|
|
3801
|
+
`Generated ${generated.stats.suggestionsGenerated} suggestions; created ${persisted.created} pending review items; skipped ${persisted.skippedExisting} existing.`
|
|
3802
|
+
);
|
|
3803
|
+
} finally {
|
|
3804
|
+
store.close();
|
|
3805
|
+
}
|
|
3806
|
+
})
|
|
3807
|
+
).addCommand(
|
|
3808
|
+
new Command30("list").description("List curation suggestions by review status").option("--status <statuses>", "Comma-separated statuses (default: pending)").option("--limit <n>", "Maximum suggestions to show", (value) => Number.parseInt(value, 10), 20).action((opts) => {
|
|
3809
|
+
const { store, orgId, repoId } = openStore();
|
|
3810
|
+
try {
|
|
3811
|
+
const status = parseStatus(opts.status);
|
|
3812
|
+
const suggestions = store.listCurationSuggestions({
|
|
3813
|
+
orgId,
|
|
3814
|
+
repoId,
|
|
3815
|
+
status,
|
|
3816
|
+
limit: opts.limit
|
|
3817
|
+
});
|
|
3818
|
+
if (isJsonMode()) {
|
|
3819
|
+
outputJson({ suggestions });
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
const label = status?.join(",") ?? "all";
|
|
3823
|
+
logger.info(`${label === "pending" ? "Pending" : label} curation suggestions`);
|
|
3824
|
+
if (suggestions.length === 0) {
|
|
3825
|
+
logger.info("No suggestions found.");
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
for (const suggestion of suggestions) {
|
|
3829
|
+
logger.info(formatSuggestionLine(suggestion));
|
|
3830
|
+
logger.info("");
|
|
3831
|
+
}
|
|
3832
|
+
} finally {
|
|
3833
|
+
store.close();
|
|
3834
|
+
}
|
|
3835
|
+
})
|
|
3836
|
+
).addCommand(
|
|
3837
|
+
new Command30("review").description("Approve, reject, or mark a curation suggestion as applied").argument("<id>", "Suggestion id").option("--approve", "Mark suggestion as approved").option("--reject", "Mark suggestion as rejected").option("--applied", "Mark suggestion as applied").option("--reviewer <name>", "Reviewer name").option("--note <note>", "Review note").action((id, opts) => {
|
|
3838
|
+
const selected = [opts.approve, opts.reject, opts.applied].filter(Boolean).length;
|
|
3839
|
+
if (selected !== 1) {
|
|
3840
|
+
logger.error("Choose exactly one of --approve, --reject, or --applied.");
|
|
3841
|
+
process.exit(EXIT_ERROR);
|
|
3842
|
+
}
|
|
3843
|
+
const status = opts.approve ? "approved" : opts.reject ? "rejected" : "applied";
|
|
3844
|
+
const { store } = openStore();
|
|
3845
|
+
try {
|
|
3846
|
+
const suggestion = store.reviewCurationSuggestion({
|
|
3847
|
+
id,
|
|
3848
|
+
status,
|
|
3849
|
+
reviewedBy: opts.reviewer,
|
|
3850
|
+
reviewNote: opts.note
|
|
3851
|
+
});
|
|
3852
|
+
if (isJsonMode()) {
|
|
3853
|
+
outputJson(suggestion);
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
logger.info(`Suggestion ${suggestion.id.slice(0, 8)} marked ${suggestion.status}.`);
|
|
3857
|
+
} finally {
|
|
3858
|
+
store.close();
|
|
3859
|
+
}
|
|
3860
|
+
})
|
|
3861
|
+
);
|
|
3862
|
+
|
|
3644
3863
|
// src/index.ts
|
|
3645
3864
|
import { createRequire } from "module";
|
|
3646
3865
|
var require2 = createRequire(import.meta.url);
|
|
@@ -3679,7 +3898,7 @@ process.on("unhandledRejection", (err) => {
|
|
|
3679
3898
|
runCleanup();
|
|
3680
3899
|
process.exit(EXIT_ERROR);
|
|
3681
3900
|
});
|
|
3682
|
-
var program = new
|
|
3901
|
+
var program = new Command31();
|
|
3683
3902
|
program.name("unforgit").description("Unforgit \u2014 repository memory for agents and developers").version(pkg.version).option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--json", "Output results as JSON (for scripting)").hook("preAction", () => {
|
|
3684
3903
|
const opts = program.opts();
|
|
3685
3904
|
if (opts.quiet) setVerbosity(0);
|
|
@@ -3717,8 +3936,10 @@ program.addCommand(configCommand);
|
|
|
3717
3936
|
program.addCommand(embeddingsCommand);
|
|
3718
3937
|
program.addCommand(resetCommand);
|
|
3719
3938
|
program.addCommand(backupsCommand);
|
|
3939
|
+
program.addCommand(dashboardCommand);
|
|
3720
3940
|
program.addCommand(doctorCommand);
|
|
3721
3941
|
program.addCommand(curateCommand);
|
|
3942
|
+
program.addCommand(suggestionsCommand);
|
|
3722
3943
|
program.addCommand(completionCommand);
|
|
3723
3944
|
program.parseAsync().catch((err) => {
|
|
3724
3945
|
console.error(`fatal: ${err instanceof Error ? err.message : err}`);
|