@iamem/amem 0.1.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/LICENSE +21 -0
- package/README.md +534 -0
- package/dist/activity.d.ts +27 -0
- package/dist/activity.js +202 -0
- package/dist/api/routes.d.ts +31 -0
- package/dist/api/routes.js +1345 -0
- package/dist/attest.d.ts +52 -0
- package/dist/attest.js +192 -0
- package/dist/backup-schedule.d.ts +25 -0
- package/dist/backup-schedule.js +216 -0
- package/dist/capture.d.ts +34 -0
- package/dist/capture.js +257 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1007 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +334 -0
- package/dist/crypto.d.ts +39 -0
- package/dist/crypto.js +166 -0
- package/dist/db.d.ts +192 -0
- package/dist/db.js +666 -0
- package/dist/draft-quality.d.ts +19 -0
- package/dist/draft-quality.js +85 -0
- package/dist/embed.d.ts +76 -0
- package/dist/embed.js +331 -0
- package/dist/estimate.d.ts +32 -0
- package/dist/estimate.js +69 -0
- package/dist/freshness.d.ts +15 -0
- package/dist/freshness.js +93 -0
- package/dist/hook.d.ts +16 -0
- package/dist/hook.js +177 -0
- package/dist/hygiene-schedule.d.ts +28 -0
- package/dist/hygiene-schedule.js +221 -0
- package/dist/hygiene.d.ts +61 -0
- package/dist/hygiene.js +196 -0
- package/dist/install/claude.d.ts +6 -0
- package/dist/install/claude.js +69 -0
- package/dist/install/cursor.d.ts +7 -0
- package/dist/install/cursor.js +80 -0
- package/dist/install/hosts.d.ts +21 -0
- package/dist/install/hosts.js +186 -0
- package/dist/install/skills.d.ts +8 -0
- package/dist/install/skills.js +67 -0
- package/dist/it-pack.d.ts +20 -0
- package/dist/it-pack.js +84 -0
- package/dist/kinds.d.ts +18 -0
- package/dist/kinds.js +106 -0
- package/dist/license.d.ts +48 -0
- package/dist/license.js +172 -0
- package/dist/mcp.d.ts +40 -0
- package/dist/mcp.js +435 -0
- package/dist/paths.d.ts +11 -0
- package/dist/paths.js +55 -0
- package/dist/personal.d.ts +7 -0
- package/dist/personal.js +44 -0
- package/dist/platforms.d.ts +11 -0
- package/dist/platforms.js +32 -0
- package/dist/policy.d.ts +46 -0
- package/dist/policy.js +254 -0
- package/dist/prefs.d.ts +6 -0
- package/dist/prefs.js +11 -0
- package/dist/proposal.d.ts +90 -0
- package/dist/proposal.js +376 -0
- package/dist/publish.d.ts +28 -0
- package/dist/publish.js +57 -0
- package/dist/remember-contract.d.ts +23 -0
- package/dist/remember-contract.js +117 -0
- package/dist/repo-identity.d.ts +15 -0
- package/dist/repo-identity.js +82 -0
- package/dist/rules-sync.d.ts +7 -0
- package/dist/rules-sync.js +47 -0
- package/dist/savings-export.d.ts +49 -0
- package/dist/savings-export.js +141 -0
- package/dist/scan.d.ts +16 -0
- package/dist/scan.js +109 -0
- package/dist/search.d.ts +25 -0
- package/dist/search.js +150 -0
- package/dist/service.d.ts +20 -0
- package/dist/service.js +254 -0
- package/dist/shop.d.ts +9 -0
- package/dist/shop.js +15 -0
- package/dist/ui/server.d.ts +21 -0
- package/dist/ui/server.js +268 -0
- package/dist/vault.d.ts +25 -0
- package/dist/vault.js +42 -0
- package/dist/workspace-setup.d.ts +8 -0
- package/dist/workspace-setup.js +55 -0
- package/docs/agent-install-prompt.md +41 -0
- package/docs/backlog.md +59 -0
- package/docs/enterprise-endpoint.md +98 -0
- package/docs/license.md +54 -0
- package/docs/npm-release.md +38 -0
- package/docs/remember-contract.md +42 -0
- package/package.json +64 -0
- package/scripts/mdm-offboard.sh +14 -0
- package/skills/amem-bootstrap/SKILL.md +83 -0
- package/skills/amem-update-working-memory/SKILL.md +54 -0
- package/templates/cursor-rule.mdc +23 -0
- package/templates/mdm/co.amem.managed.plist +33 -0
- package/templates/policy.deny-default.toml +20 -0
- package/templates/policy.example.toml +22 -0
- package/ui-static/app.js +3966 -0
- package/ui-static/index.html +178 -0
- package/ui-static/orbit.js +389 -0
- package/ui-static/styles.css +2411 -0
package/dist/attest.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type LoadedPolicy } from "./policy.js";
|
|
2
|
+
import { licenseStatus } from "./license.js";
|
|
3
|
+
import { embedStatus } from "./embed.js";
|
|
4
|
+
import { vaultStatus } from "./vault.js";
|
|
5
|
+
export type AttestReport = {
|
|
6
|
+
tool: "amem";
|
|
7
|
+
version: string;
|
|
8
|
+
package_integrity: {
|
|
9
|
+
package_json_sha256: string | null;
|
|
10
|
+
};
|
|
11
|
+
attested_at: string;
|
|
12
|
+
privacy: {
|
|
13
|
+
telemetry: false;
|
|
14
|
+
network_egress: "none";
|
|
15
|
+
ui_bind: string;
|
|
16
|
+
ui_enabled: boolean;
|
|
17
|
+
memory_home: string;
|
|
18
|
+
memory_home_mode: string | null;
|
|
19
|
+
db_path: string;
|
|
20
|
+
db_exists: boolean;
|
|
21
|
+
};
|
|
22
|
+
policy: {
|
|
23
|
+
sources: LoadedPolicy["sources"];
|
|
24
|
+
effective: LoadedPolicy["policy"];
|
|
25
|
+
builtin_deny_claim_patterns: string[];
|
|
26
|
+
effective_deny_claim_patterns: string[];
|
|
27
|
+
};
|
|
28
|
+
repo: {
|
|
29
|
+
root_path: string;
|
|
30
|
+
repo_key: string;
|
|
31
|
+
remote_url: string | null;
|
|
32
|
+
bound: boolean;
|
|
33
|
+
platform: string | null;
|
|
34
|
+
};
|
|
35
|
+
platforms: {
|
|
36
|
+
cursor_issues: string[];
|
|
37
|
+
claude_issues: string[];
|
|
38
|
+
};
|
|
39
|
+
license: ReturnType<typeof licenseStatus>;
|
|
40
|
+
embed: ReturnType<typeof embedStatus>;
|
|
41
|
+
sku?: {
|
|
42
|
+
tier: string;
|
|
43
|
+
airgap: true;
|
|
44
|
+
network_egress: "none";
|
|
45
|
+
vault: ReturnType<typeof vaultStatus>;
|
|
46
|
+
host_health: Record<string, string[]>;
|
|
47
|
+
};
|
|
48
|
+
ok: boolean;
|
|
49
|
+
issues: string[];
|
|
50
|
+
};
|
|
51
|
+
export declare function buildAttestReport(cwd?: string): AttestReport;
|
|
52
|
+
export declare function formatAttestHuman(report: AttestReport): string;
|
package/dist/attest.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getRepoByCwd, getSetupState, openDb } from "./db.js";
|
|
6
|
+
import { claudeInstallHealth } from "./install/claude.js";
|
|
7
|
+
import { cursorInstallHealth } from "./install/cursor.js";
|
|
8
|
+
import { amemHome, dbPath, ensureAmemHome } from "./paths.js";
|
|
9
|
+
import { BUILTIN_DENY_CLAIM_PATTERNS, effectiveDenyPatterns, loadPolicy, } from "./policy.js";
|
|
10
|
+
import { detectRepoIdentity } from "./repo-identity.js";
|
|
11
|
+
import { FEATURE_ATTEST_SKU, hasFeature, licenseStatus } from "./license.js";
|
|
12
|
+
import { embedIndexIssues, embedStatus } from "./embed.js";
|
|
13
|
+
import { vaultStatus } from "./vault.js";
|
|
14
|
+
import { hostInstallHealth } from "./install/hosts.js";
|
|
15
|
+
function packageRoot() {
|
|
16
|
+
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
17
|
+
// dist/ -> package root
|
|
18
|
+
return join(here, "..");
|
|
19
|
+
}
|
|
20
|
+
function readVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot(), "package.json"), "utf8"));
|
|
23
|
+
return pkg.version ?? "0.0.0";
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "0.0.0";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function sha256File(path) {
|
|
30
|
+
try {
|
|
31
|
+
if (!existsSync(path))
|
|
32
|
+
return null;
|
|
33
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function modeOctal(path) {
|
|
40
|
+
try {
|
|
41
|
+
if (!existsSync(path))
|
|
42
|
+
return null;
|
|
43
|
+
return (statSync(path).mode & 0o777).toString(8).padStart(3, "0");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function buildAttestReport(cwd = process.cwd()) {
|
|
50
|
+
ensureAmemHome();
|
|
51
|
+
const loaded = loadPolicy(true);
|
|
52
|
+
const identity = detectRepoIdentity(cwd);
|
|
53
|
+
const repo = getRepoByCwd(cwd);
|
|
54
|
+
const issues = [];
|
|
55
|
+
for (const src of loaded.sources) {
|
|
56
|
+
if (src.error)
|
|
57
|
+
issues.push(`Policy ${src.role} (${src.path}): ${src.error}`);
|
|
58
|
+
}
|
|
59
|
+
if (loaded.policy.telemetry) {
|
|
60
|
+
issues.push("telemetry unexpectedly enabled (hard-stop failed)");
|
|
61
|
+
}
|
|
62
|
+
if (loaded.policy.ui_bind !== "127.0.0.1" && loaded.policy.ui_bind !== "localhost") {
|
|
63
|
+
issues.push(`ui_bind must be loopback, got ${loaded.policy.ui_bind}`);
|
|
64
|
+
}
|
|
65
|
+
const homeMode = modeOctal(amemHome());
|
|
66
|
+
if (homeMode && homeMode !== "700") {
|
|
67
|
+
issues.push(`amem home mode is ${homeMode}, expected 700`);
|
|
68
|
+
}
|
|
69
|
+
let cursorIssues = [];
|
|
70
|
+
let claudeIssues = [];
|
|
71
|
+
let platforms = [];
|
|
72
|
+
if (repo) {
|
|
73
|
+
try {
|
|
74
|
+
const setup = getSetupState(repo.id);
|
|
75
|
+
platforms = setup ? JSON.parse(setup.platforms) : [];
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
platforms = [];
|
|
79
|
+
}
|
|
80
|
+
if (platforms.length === 0 && repo.platform)
|
|
81
|
+
platforms = [repo.platform];
|
|
82
|
+
}
|
|
83
|
+
const checkCursor = platforms.includes("cursor") || repo?.platform === "cursor" || platforms.length === 0;
|
|
84
|
+
const checkClaude = platforms.includes("claude") || repo?.platform === "claude" || platforms.length === 0;
|
|
85
|
+
if (repo) {
|
|
86
|
+
if (checkCursor)
|
|
87
|
+
cursorIssues = cursorInstallHealth(identity.rootPath);
|
|
88
|
+
if (checkClaude)
|
|
89
|
+
claudeIssues = claudeInstallHealth();
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
issues.push("Repo not initialized — run amem init");
|
|
93
|
+
}
|
|
94
|
+
issues.push(...cursorIssues, ...claudeIssues);
|
|
95
|
+
const license = licenseStatus();
|
|
96
|
+
issues.push(...license.issues.map((i) => `license: ${i}`));
|
|
97
|
+
const embed = embedStatus();
|
|
98
|
+
if (embed.requested === "ngram" && !embed.licensed) {
|
|
99
|
+
issues.push("embed_backend ngram requested but license is not Pro/IT — using hash");
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
issues.push(...embedIndexIssues(openDb()));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Locked vault: the vault section already reports that.
|
|
106
|
+
}
|
|
107
|
+
const pkgPath = join(packageRoot(), "package.json");
|
|
108
|
+
const sku = hasFeature(FEATURE_ATTEST_SKU)
|
|
109
|
+
? {
|
|
110
|
+
tier: license.tier,
|
|
111
|
+
airgap: true,
|
|
112
|
+
network_egress: "none",
|
|
113
|
+
vault: vaultStatus(),
|
|
114
|
+
host_health: {
|
|
115
|
+
continue: hostInstallHealth("continue"),
|
|
116
|
+
zed: hostInstallHealth("zed"),
|
|
117
|
+
windsurf: hostInstallHealth("windsurf"),
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
: undefined;
|
|
121
|
+
return {
|
|
122
|
+
tool: "amem",
|
|
123
|
+
version: readVersion(),
|
|
124
|
+
package_integrity: {
|
|
125
|
+
package_json_sha256: sha256File(pkgPath),
|
|
126
|
+
},
|
|
127
|
+
attested_at: new Date().toISOString(),
|
|
128
|
+
privacy: {
|
|
129
|
+
telemetry: false,
|
|
130
|
+
network_egress: "none",
|
|
131
|
+
ui_bind: loaded.policy.ui_bind,
|
|
132
|
+
ui_enabled: loaded.policy.ui_enabled,
|
|
133
|
+
memory_home: amemHome(),
|
|
134
|
+
memory_home_mode: homeMode,
|
|
135
|
+
db_path: dbPath(),
|
|
136
|
+
db_exists: existsSync(dbPath()),
|
|
137
|
+
},
|
|
138
|
+
policy: {
|
|
139
|
+
sources: loaded.sources,
|
|
140
|
+
effective: loaded.policy,
|
|
141
|
+
builtin_deny_claim_patterns: [...BUILTIN_DENY_CLAIM_PATTERNS],
|
|
142
|
+
effective_deny_claim_patterns: effectiveDenyPatterns(loaded.policy),
|
|
143
|
+
},
|
|
144
|
+
repo: {
|
|
145
|
+
root_path: identity.rootPath,
|
|
146
|
+
repo_key: identity.repoKey,
|
|
147
|
+
remote_url: identity.remoteUrl,
|
|
148
|
+
bound: Boolean(repo),
|
|
149
|
+
platform: repo?.platform ?? null,
|
|
150
|
+
},
|
|
151
|
+
platforms: {
|
|
152
|
+
cursor_issues: cursorIssues,
|
|
153
|
+
claude_issues: claudeIssues,
|
|
154
|
+
},
|
|
155
|
+
license,
|
|
156
|
+
embed,
|
|
157
|
+
sku,
|
|
158
|
+
ok: issues.length === 0,
|
|
159
|
+
issues,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export function formatAttestHuman(report) {
|
|
163
|
+
const lines = [
|
|
164
|
+
`amem attest ${report.ok ? "OK" : "ISSUES"} · v${report.version}`,
|
|
165
|
+
`attested_at: ${report.attested_at}`,
|
|
166
|
+
`memory_home: ${report.privacy.memory_home} (mode ${report.privacy.memory_home_mode ?? "n/a"})`,
|
|
167
|
+
`db: ${report.privacy.db_path} (${report.privacy.db_exists ? "present" : "absent"})`,
|
|
168
|
+
`telemetry: ${report.privacy.telemetry}`,
|
|
169
|
+
`network_egress: ${report.privacy.network_egress}`,
|
|
170
|
+
`ui: enabled=${report.privacy.ui_enabled} bind=${report.privacy.ui_bind}`,
|
|
171
|
+
`allow_export: ${report.policy.effective.allow_export}`,
|
|
172
|
+
`allowed_platforms: ${report.policy.effective.allowed_platforms.join(", ") || "(any)"}`,
|
|
173
|
+
`allowed_remote_hosts: ${report.policy.effective.allowed_remote_hosts.join(", ") || "(any)"}`,
|
|
174
|
+
`deny_patterns: ${report.policy.effective_deny_claim_patterns.length} (builtin + policy)`,
|
|
175
|
+
`repo: ${report.repo.root_path}`,
|
|
176
|
+
`remote: ${report.repo.remote_url ?? "(none)"}`,
|
|
177
|
+
`bound: ${report.repo.bound}`,
|
|
178
|
+
`license: ${report.license.tier} (${report.license.kind}${report.license.valid ? "" : ", invalid"})`,
|
|
179
|
+
`embed: ${report.embed.backend} dim=${report.embed.dim}`,
|
|
180
|
+
];
|
|
181
|
+
if (report.sku) {
|
|
182
|
+
lines.push(`sku: IT airgap packet · vault locked=${report.sku.vault.encryptedAtRest} · backup=${report.sku.vault.backup.scheduled ? "scheduled" : "off"}`);
|
|
183
|
+
}
|
|
184
|
+
const applied = report.policy.sources.filter((s) => s.applied).map((s) => `${s.role}:${s.path}`);
|
|
185
|
+
lines.push(`policy_sources: ${applied.join(" → ")}`);
|
|
186
|
+
if (report.issues.length) {
|
|
187
|
+
lines.push("issues:");
|
|
188
|
+
for (const issue of report.issues)
|
|
189
|
+
lines.push(`- ${issue}`);
|
|
190
|
+
}
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const BACKUP_LABEL = "co.amem.backup";
|
|
2
|
+
export declare const BACKUP_SYSTEMD_SERVICE = "amem-backup.service";
|
|
3
|
+
export declare const BACKUP_SYSTEMD_TIMER = "amem-backup.timer";
|
|
4
|
+
export type BackupScheduleOpts = {
|
|
5
|
+
outDir?: string;
|
|
6
|
+
/** Hour of day local time 0–23 (default 3). */
|
|
7
|
+
hour?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function backupLaunchAgentPath(): string;
|
|
10
|
+
export declare function backupSystemdServicePath(): string;
|
|
11
|
+
export declare function backupSystemdTimerPath(): string;
|
|
12
|
+
export declare function backupWindowsStartupPath(): string;
|
|
13
|
+
export declare function isBackupScheduleInstalled(): boolean;
|
|
14
|
+
export declare function backupSchedulePath(): string;
|
|
15
|
+
export declare function installBackupSchedule(opts?: BackupScheduleOpts): {
|
|
16
|
+
path: string;
|
|
17
|
+
outDir: string;
|
|
18
|
+
platform: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function uninstallBackupSchedule(): {
|
|
21
|
+
path: string;
|
|
22
|
+
platform: string;
|
|
23
|
+
};
|
|
24
|
+
/** Write a tiny wrapper script under ~/.amem for manual cron users. */
|
|
25
|
+
export declare function writeBackupHelperScript(outDir?: string): string;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schedule local encrypted backups (no cloud sync).
|
|
3
|
+
* Uses LaunchAgent (macOS), systemd user timer (Linux), or Startup cmd (Windows).
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync, chmodSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { defaultBackupDir } from "./crypto.js";
|
|
10
|
+
import { amemHome } from "./paths.js";
|
|
11
|
+
import { resolveAmemProgramArgs } from "./install/skills.js";
|
|
12
|
+
export const BACKUP_LABEL = "co.amem.backup";
|
|
13
|
+
export const BACKUP_SYSTEMD_SERVICE = "amem-backup.service";
|
|
14
|
+
export const BACKUP_SYSTEMD_TIMER = "amem-backup.timer";
|
|
15
|
+
function escapeXml(value) {
|
|
16
|
+
return value
|
|
17
|
+
.replace(/&/g, "&")
|
|
18
|
+
.replace(/</g, "<")
|
|
19
|
+
.replace(/>/g, ">")
|
|
20
|
+
.replace(/"/g, """);
|
|
21
|
+
}
|
|
22
|
+
function backupArgs(outDir) {
|
|
23
|
+
return resolveAmemProgramArgs("backup", "--out", outDir);
|
|
24
|
+
}
|
|
25
|
+
function shellQuote(args) {
|
|
26
|
+
return args.map((a) => (/\s/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a)).join(" ");
|
|
27
|
+
}
|
|
28
|
+
export function backupLaunchAgentPath() {
|
|
29
|
+
return join(homedir(), "Library", "LaunchAgents", `${BACKUP_LABEL}.plist`);
|
|
30
|
+
}
|
|
31
|
+
export function backupSystemdServicePath() {
|
|
32
|
+
return join(homedir(), ".config", "systemd", "user", BACKUP_SYSTEMD_SERVICE);
|
|
33
|
+
}
|
|
34
|
+
export function backupSystemdTimerPath() {
|
|
35
|
+
return join(homedir(), ".config", "systemd", "user", BACKUP_SYSTEMD_TIMER);
|
|
36
|
+
}
|
|
37
|
+
export function backupWindowsStartupPath() {
|
|
38
|
+
const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
|
|
39
|
+
return join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "amem-backup.cmd");
|
|
40
|
+
}
|
|
41
|
+
export function isBackupScheduleInstalled() {
|
|
42
|
+
if (process.platform === "darwin")
|
|
43
|
+
return existsSync(backupLaunchAgentPath());
|
|
44
|
+
if (process.platform === "linux")
|
|
45
|
+
return existsSync(backupSystemdTimerPath());
|
|
46
|
+
if (process.platform === "win32")
|
|
47
|
+
return existsSync(backupWindowsStartupPath());
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
export function backupSchedulePath() {
|
|
51
|
+
if (process.platform === "darwin")
|
|
52
|
+
return backupLaunchAgentPath();
|
|
53
|
+
if (process.platform === "linux")
|
|
54
|
+
return backupSystemdTimerPath();
|
|
55
|
+
if (process.platform === "win32")
|
|
56
|
+
return backupWindowsStartupPath();
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
function uid() {
|
|
60
|
+
return String(process.getuid?.() ?? "");
|
|
61
|
+
}
|
|
62
|
+
export function installBackupSchedule(opts = {}) {
|
|
63
|
+
const outDir = opts.outDir || defaultBackupDir();
|
|
64
|
+
mkdirSync(outDir, { recursive: true, mode: 0o700 });
|
|
65
|
+
const hour = Math.min(23, Math.max(0, opts.hour ?? 3));
|
|
66
|
+
const args = backupArgs(outDir);
|
|
67
|
+
if (process.platform === "darwin") {
|
|
68
|
+
const path = backupLaunchAgentPath();
|
|
69
|
+
mkdirSync(join(homedir(), "Library", "LaunchAgents"), { recursive: true });
|
|
70
|
+
const argXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
71
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
72
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
73
|
+
<plist version="1.0">
|
|
74
|
+
<dict>
|
|
75
|
+
<key>Label</key>
|
|
76
|
+
<string>${BACKUP_LABEL}</string>
|
|
77
|
+
<key>ProgramArguments</key>
|
|
78
|
+
<array>
|
|
79
|
+
${argXml}
|
|
80
|
+
</array>
|
|
81
|
+
<key>StartCalendarInterval</key>
|
|
82
|
+
<dict>
|
|
83
|
+
<key>Hour</key>
|
|
84
|
+
<integer>${hour}</integer>
|
|
85
|
+
<key>Minute</key>
|
|
86
|
+
<integer>15</integer>
|
|
87
|
+
</dict>
|
|
88
|
+
<key>StandardOutPath</key>
|
|
89
|
+
<string>${escapeXml(join(amemHome(), "backup.log"))}</string>
|
|
90
|
+
<key>StandardErrorPath</key>
|
|
91
|
+
<string>${escapeXml(join(amemHome(), "backup.err.log"))}</string>
|
|
92
|
+
<key>EnvironmentVariables</key>
|
|
93
|
+
<dict>
|
|
94
|
+
<key>PATH</key>
|
|
95
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
96
|
+
</dict>
|
|
97
|
+
</dict>
|
|
98
|
+
</plist>
|
|
99
|
+
`;
|
|
100
|
+
writeFileSync(path, body, "utf8");
|
|
101
|
+
try {
|
|
102
|
+
execFileSync("launchctl", ["bootout", `gui/${uid()}`, path], {
|
|
103
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// not loaded
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
execFileSync("launchctl", ["bootstrap", `gui/${uid()}`, path], {
|
|
111
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// next login picks it up
|
|
116
|
+
}
|
|
117
|
+
return { path, outDir, platform: "darwin" };
|
|
118
|
+
}
|
|
119
|
+
if (process.platform === "linux") {
|
|
120
|
+
const unitDir = join(homedir(), ".config", "systemd", "user");
|
|
121
|
+
mkdirSync(unitDir, { recursive: true });
|
|
122
|
+
const svc = backupSystemdServicePath();
|
|
123
|
+
const timer = backupSystemdTimerPath();
|
|
124
|
+
writeFileSync(svc, `[Unit]
|
|
125
|
+
Description=amem local encrypted backup
|
|
126
|
+
[Service]
|
|
127
|
+
Type=oneshot
|
|
128
|
+
ExecStart=${shellQuote(args)}
|
|
129
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homedir()}/.local/bin
|
|
130
|
+
`, "utf8");
|
|
131
|
+
writeFileSync(timer, `[Unit]
|
|
132
|
+
Description=Daily amem local backup
|
|
133
|
+
[Timer]
|
|
134
|
+
OnCalendar=*-*-* ${String(hour).padStart(2, "0")}:15:00
|
|
135
|
+
Persistent=true
|
|
136
|
+
[Install]
|
|
137
|
+
WantedBy=timers.target
|
|
138
|
+
`, "utf8");
|
|
139
|
+
try {
|
|
140
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
141
|
+
execFileSync("systemctl", ["--user", "enable", "--now", BACKUP_SYSTEMD_TIMER], {
|
|
142
|
+
stdio: "ignore",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// unit files are on disk
|
|
147
|
+
}
|
|
148
|
+
return { path: timer, outDir, platform: "linux" };
|
|
149
|
+
}
|
|
150
|
+
if (process.platform === "win32") {
|
|
151
|
+
const path = backupWindowsStartupPath();
|
|
152
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
153
|
+
writeFileSync(path, `@echo off\r\nrem amem daily backup (runs at login)\r\n${shellQuote(args)}\r\n`, "utf8");
|
|
154
|
+
return { path, outDir, platform: "win32" };
|
|
155
|
+
}
|
|
156
|
+
throw new Error(`Backup scheduling not supported on ${process.platform}`);
|
|
157
|
+
}
|
|
158
|
+
export function uninstallBackupSchedule() {
|
|
159
|
+
if (process.platform === "darwin") {
|
|
160
|
+
const path = backupLaunchAgentPath();
|
|
161
|
+
if (existsSync(path)) {
|
|
162
|
+
try {
|
|
163
|
+
execFileSync("launchctl", ["bootout", `gui/${uid()}`, path], {
|
|
164
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// ignore
|
|
169
|
+
}
|
|
170
|
+
unlinkSync(path);
|
|
171
|
+
}
|
|
172
|
+
return { path, platform: "darwin" };
|
|
173
|
+
}
|
|
174
|
+
if (process.platform === "linux") {
|
|
175
|
+
const timer = backupSystemdTimerPath();
|
|
176
|
+
const svc = backupSystemdServicePath();
|
|
177
|
+
try {
|
|
178
|
+
execFileSync("systemctl", ["--user", "disable", "--now", BACKUP_SYSTEMD_TIMER], {
|
|
179
|
+
stdio: "ignore",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// ignore
|
|
184
|
+
}
|
|
185
|
+
if (existsSync(timer))
|
|
186
|
+
unlinkSync(timer);
|
|
187
|
+
if (existsSync(svc))
|
|
188
|
+
unlinkSync(svc);
|
|
189
|
+
return { path: timer, platform: "linux" };
|
|
190
|
+
}
|
|
191
|
+
if (process.platform === "win32") {
|
|
192
|
+
const path = backupWindowsStartupPath();
|
|
193
|
+
if (existsSync(path))
|
|
194
|
+
unlinkSync(path);
|
|
195
|
+
return { path, platform: "win32" };
|
|
196
|
+
}
|
|
197
|
+
throw new Error(`Backup scheduling not supported on ${process.platform}`);
|
|
198
|
+
}
|
|
199
|
+
/** Write a tiny wrapper script under ~/.amem for manual cron users. */
|
|
200
|
+
export function writeBackupHelperScript(outDir) {
|
|
201
|
+
const dir = outDir || defaultBackupDir();
|
|
202
|
+
const script = join(amemHome(), "bin", "amem-backup.sh");
|
|
203
|
+
mkdirSync(join(amemHome(), "bin"), { recursive: true, mode: 0o700 });
|
|
204
|
+
const body = `#!/usr/bin/env bash
|
|
205
|
+
set -euo pipefail
|
|
206
|
+
exec ${shellQuote(backupArgs(dir))}
|
|
207
|
+
`;
|
|
208
|
+
writeFileSync(script, body, { mode: 0o700 });
|
|
209
|
+
try {
|
|
210
|
+
chmodSync(script, 0o700);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// ignore
|
|
214
|
+
}
|
|
215
|
+
return script;
|
|
216
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ProposalDraftRow, type RepoRow, type UsageEventRow } from "./db.js";
|
|
2
|
+
import { type Proposal } from "./proposal.js";
|
|
3
|
+
export declare function isUsefulCaptureText(text: string): boolean;
|
|
4
|
+
export declare function extractCaptureAnchors(text: string, repoRoot: string): string[];
|
|
5
|
+
/** High-quality durable facts apply without waiting for amem_remember. */
|
|
6
|
+
export declare function shouldAutoApplyProposal(proposal: Proposal): boolean;
|
|
7
|
+
/** Apply pending drafts (skips quality.reject). Used when auto-approve is turned on. */
|
|
8
|
+
export declare function applyPendingDrafts(repoId?: string): {
|
|
9
|
+
applied: string[];
|
|
10
|
+
skipped: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function captureSessionDraft(input: {
|
|
13
|
+
repo: RepoRow;
|
|
14
|
+
platform: string;
|
|
15
|
+
sessionId?: string | null;
|
|
16
|
+
prompt: string;
|
|
17
|
+
answer?: string;
|
|
18
|
+
notes?: Array<{
|
|
19
|
+
role: string;
|
|
20
|
+
text: string;
|
|
21
|
+
}>;
|
|
22
|
+
}): ProposalDraftRow | null;
|
|
23
|
+
export declare function findRecentContextMisses(repoId: string, opts?: {
|
|
24
|
+
sessionId?: string | null;
|
|
25
|
+
limit?: number;
|
|
26
|
+
}): UsageEventRow[];
|
|
27
|
+
export declare function captureMissLearnDraft(input: {
|
|
28
|
+
repo: RepoRow;
|
|
29
|
+
platform: string;
|
|
30
|
+
sessionId?: string | null;
|
|
31
|
+
miss: UsageEventRow;
|
|
32
|
+
answer: string;
|
|
33
|
+
}): ProposalDraftRow | null;
|
|
34
|
+
export declare function pendingDraftCount(repoId: string): number;
|