@iamem/amem 0.1.2 → 0.1.3
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 +64 -1
- package/dist/api/routes.js +331 -1
- package/dist/attest.d.ts +13 -0
- package/dist/attest.js +44 -0
- package/dist/cli.js +241 -2
- package/dist/context.d.ts +10 -1
- package/dist/context.js +105 -3
- package/dist/db.d.ts +141 -0
- package/dist/db.js +398 -0
- package/dist/embed.js +5 -14
- package/dist/hook.js +8 -1
- package/dist/hygiene.d.ts +1 -2
- package/dist/hygiene.js +1 -2
- package/dist/install/hosts.js +8 -0
- package/dist/install/skills.js +10 -5
- package/dist/license.d.ts +1 -0
- package/dist/license.js +21 -19
- package/dist/mcp.js +221 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.js +16 -1
- package/dist/skill-capture.d.ts +43 -0
- package/dist/skill-capture.js +146 -0
- package/dist/skills.d.ts +106 -0
- package/dist/skills.js +422 -0
- package/docs/backlog.md +9 -0
- package/package.json +1 -1
- package/skills/amem-tasks/SKILL.md +100 -0
- package/skills/amem-write-skill/SKILL.md +99 -0
- package/templates/cursor-rule.mdc +16 -6
- package/templates/policy.deny-default.toml +5 -0
- package/templates/policy.example.toml +8 -0
- package/ui-static/app.js +435 -227
- package/ui-static/index.html +11 -34
- package/ui-static/styles.css +299 -0
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { buildAttestReport, formatAttestHuman } from "./attest.js";
|
|
5
5
|
import { handleApi, logContextUsage } from "./api/routes.js";
|
|
6
|
-
import { closeDb, getRepoByCwd, getRepoByName, renameWorkspace, listClaims, listComponents, listFlows, requireRepo, setReportedOnLatest, setReportedTokensSaved, touchSession, upsertRepo, upsertSetupState, wipeAllRepos, wipeRepo, openDb, } from "./db.js";
|
|
6
|
+
import { closeDb, getRepoByCwd, getRepoByName, renameWorkspace, listClaims, listComponents, listFlows, listRepos, listTasks, listTasksAll, insertTask, updateTask, completeTask, deleteTask, findTaskAnyRepo, normalizeTaskStatus, requireRepo, setReportedOnLatest, setReportedTokensSaved, touchSession, upsertRepo, upsertSetupState, wipeAllRepos, wipeRepo, openDb, } from "./db.js";
|
|
7
7
|
import { handleHookPayload } from "./hook.js";
|
|
8
8
|
import { installClaude, claudeInstallHealth } from "./install/claude.js";
|
|
9
9
|
import { installCursor, cursorInstallHealth } from "./install/cursor.js";
|
|
@@ -28,6 +28,8 @@ import { embedIndexIssues, embedStatus, reindexAllEmbeds, setEmbedBackend, } fro
|
|
|
28
28
|
import { acceptSafeCleanups, decayStaleClaims, hygieneReport, mergeDuplicate, runScheduledHygiene } from "./hygiene.js";
|
|
29
29
|
import { hygieneSchedulePath, installHygieneSchedule, isHygieneScheduleInstalled, uninstallHygieneSchedule, writeHygieneHelperScript, } from "./hygiene-schedule.js";
|
|
30
30
|
import { syncPinnedRules } from "./rules-sync.js";
|
|
31
|
+
import { listSkillDrafts } from "./db.js";
|
|
32
|
+
import { deleteSkill, findSkillOnDisk, importSkillFromPath, isValidSkillName, listIndexedSkills, readSkillBody, renderSkillMarkdown, skillsDir, slugifySkillName, syncSkillIndex, writeSkill, } from "./skills.js";
|
|
31
33
|
import { buildSbom, writeItPack } from "./it-pack.js";
|
|
32
34
|
function usage() {
|
|
33
35
|
console.log(`amem — local personal agent memory
|
|
@@ -43,6 +45,11 @@ Usage:
|
|
|
43
45
|
amem context "<query>" [--workspace <name>] [--platform cursor|claude|luna]
|
|
44
46
|
amem remember "<text>" [--workspace <name>] [--kind session] [--anchor <path>]
|
|
45
47
|
amem recipe [--json]
|
|
48
|
+
amem task [list] [--status <backlog|next|doing|blocked|done>] [--include-done] [--all] [--json]
|
|
49
|
+
amem task add <title> [--body <notes>] [--status <status>] [--anchor <file>]
|
|
50
|
+
amem task update <id> [--title <title>] [--body <notes>] [--status <status>]
|
|
51
|
+
amem task complete <id>
|
|
52
|
+
amem task delete <id>
|
|
46
53
|
amem propose validate <file.json>
|
|
47
54
|
amem propose diff <file.json>
|
|
48
55
|
amem propose apply <file.json>
|
|
@@ -60,6 +67,8 @@ Usage:
|
|
|
60
67
|
amem hygiene schedule [--hour <0-23>]
|
|
61
68
|
amem hygiene unschedule
|
|
62
69
|
amem rules sync
|
|
70
|
+
amem skills list|show <name>|new <name> [--desc <text>]|rm <name>|sync|import <path>
|
|
71
|
+
amem skills drafts|approve <id>|dismiss <id>
|
|
63
72
|
amem it-pack [--out <dir>]
|
|
64
73
|
amem doctor [--attest] [--sbom] [--json]
|
|
65
74
|
amem session touch --platform cursor|claude [--session-id <id>]
|
|
@@ -691,6 +700,235 @@ async function main() {
|
|
|
691
700
|
console.log("Keep this file out of shared git if it contains personal notes.");
|
|
692
701
|
break;
|
|
693
702
|
}
|
|
703
|
+
case "task":
|
|
704
|
+
case "tasks": {
|
|
705
|
+
const sub = positional[1] || "list";
|
|
706
|
+
if (sub === "list") {
|
|
707
|
+
const isAll = Boolean(flags.get("all"));
|
|
708
|
+
const statusRaw = flagString(flags, "status");
|
|
709
|
+
const status = statusRaw ? (normalizeTaskStatus(statusRaw) ?? undefined) : undefined;
|
|
710
|
+
const includeDone = Boolean(flags.get("include-done") || flags.get("all") || status === "done");
|
|
711
|
+
let tasks;
|
|
712
|
+
if (isAll) {
|
|
713
|
+
tasks = listTasksAll({ status, includeDone });
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
let repo;
|
|
717
|
+
try {
|
|
718
|
+
repo = resolveBinding(flags);
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
repo = getRepoByCwd() || ensurePersonalWorkspace();
|
|
722
|
+
}
|
|
723
|
+
tasks = listTasks(repo.id, { status, includeDone });
|
|
724
|
+
}
|
|
725
|
+
if (flags.get("json")) {
|
|
726
|
+
console.log(JSON.stringify({ tasks }, null, 2));
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
if (tasks.length === 0) {
|
|
730
|
+
console.log("No tasks found.");
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
const repoMap = new Map(listRepos().map((r) => [r.id, r.repo_name]));
|
|
734
|
+
for (const t of tasks) {
|
|
735
|
+
const repoLabel = isAll ? ` [${repoMap.get(t.repo_id) || "repo"}]` : "";
|
|
736
|
+
const body = t.body ? ` — ${t.body}` : "";
|
|
737
|
+
console.log(`[${t.status}] ${t.title}${body}${repoLabel} (${t.id})`);
|
|
738
|
+
}
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
if (sub === "add") {
|
|
742
|
+
const title = positional.slice(2).join(" ") || flagString(flags, "title");
|
|
743
|
+
if (!title || !title.trim()) {
|
|
744
|
+
throw new Error("Usage: amem task add <title> [--body <notes>] [--status <status>] [--anchor <file>]");
|
|
745
|
+
}
|
|
746
|
+
let repo;
|
|
747
|
+
try {
|
|
748
|
+
repo = resolveBinding(flags);
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
repo = getRepoByCwd() || ensurePersonalWorkspace();
|
|
752
|
+
}
|
|
753
|
+
const body = flagString(flags, "body") || "";
|
|
754
|
+
const status = flagString(flags, "status") || "backlog";
|
|
755
|
+
const anchor = flagString(flags, "anchor");
|
|
756
|
+
const anchors = anchor ? [anchor] : undefined;
|
|
757
|
+
const task = insertTask({
|
|
758
|
+
repoId: repo.id,
|
|
759
|
+
title: title.trim(),
|
|
760
|
+
body,
|
|
761
|
+
status,
|
|
762
|
+
anchors,
|
|
763
|
+
source: "cli",
|
|
764
|
+
});
|
|
765
|
+
console.log(`Created task ${task.id} [${task.status}]: ${task.title}`);
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
if (sub === "update") {
|
|
769
|
+
const id = positional[2];
|
|
770
|
+
if (!id)
|
|
771
|
+
throw new Error("Usage: amem task update <id> [--title <title>] [--body <notes>] [--status <status>]");
|
|
772
|
+
const found = findTaskAnyRepo(id);
|
|
773
|
+
if (!found)
|
|
774
|
+
throw new Error(`Task not found: ${id}`);
|
|
775
|
+
const title = flagString(flags, "title");
|
|
776
|
+
const body = flagString(flags, "body");
|
|
777
|
+
const status = flagString(flags, "status");
|
|
778
|
+
const anchor = flagString(flags, "anchor");
|
|
779
|
+
const anchors = anchor ? [anchor] : undefined;
|
|
780
|
+
const updated = updateTask(found.repo_id, id, {
|
|
781
|
+
title,
|
|
782
|
+
body,
|
|
783
|
+
status,
|
|
784
|
+
anchors,
|
|
785
|
+
});
|
|
786
|
+
if (!updated)
|
|
787
|
+
throw new Error(`Task not found: ${id}`);
|
|
788
|
+
console.log(`Updated task ${updated.id} [${updated.status}]: ${updated.title}`);
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
if (sub === "complete" || sub === "done") {
|
|
792
|
+
const id = positional[2];
|
|
793
|
+
if (!id)
|
|
794
|
+
throw new Error("Usage: amem task complete <id>");
|
|
795
|
+
const found = findTaskAnyRepo(id);
|
|
796
|
+
if (!found)
|
|
797
|
+
throw new Error(`Task not found: ${id}`);
|
|
798
|
+
const completed = completeTask(found.repo_id, id);
|
|
799
|
+
if (!completed)
|
|
800
|
+
throw new Error(`Task not found: ${id}`);
|
|
801
|
+
console.log(`Completed task ${completed.id}: ${completed.title}`);
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
if (sub === "rm" || sub === "delete") {
|
|
805
|
+
const id = positional[2];
|
|
806
|
+
if (!id)
|
|
807
|
+
throw new Error("Usage: amem task delete <id>");
|
|
808
|
+
const found = findTaskAnyRepo(id);
|
|
809
|
+
if (!found)
|
|
810
|
+
throw new Error(`Task not found: ${id}`);
|
|
811
|
+
const ok = deleteTask(found.repo_id, id);
|
|
812
|
+
if (!ok)
|
|
813
|
+
throw new Error(`Task not found: ${id}`);
|
|
814
|
+
console.log(`Deleted task ${id}`);
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
throw new Error("Usage: amem task list|add|update|complete|delete");
|
|
818
|
+
}
|
|
819
|
+
case "skills": {
|
|
820
|
+
const sub = positional[1] || "list";
|
|
821
|
+
if (sub === "list") {
|
|
822
|
+
const skills = listIndexedSkills();
|
|
823
|
+
if (skills.length === 0) {
|
|
824
|
+
console.log(`No skills yet. Create one with: amem skills new <name>`);
|
|
825
|
+
console.log(`Skills live in ${skillsDir()}`);
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
for (const s of skills) {
|
|
829
|
+
const flag = s.modified ? " (edited)" : "";
|
|
830
|
+
const used = s.uses > 0 ? ` · used ${s.uses}×` : "";
|
|
831
|
+
console.log(`${s.name}${flag}${used}`);
|
|
832
|
+
if (s.description)
|
|
833
|
+
console.log(` ${s.description}`);
|
|
834
|
+
}
|
|
835
|
+
console.log(`\n${skills.length} skill(s) in ${skillsDir()}`);
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
if (sub === "show") {
|
|
839
|
+
const name = positional[2];
|
|
840
|
+
if (!name)
|
|
841
|
+
throw new Error("Usage: amem skills show <name>");
|
|
842
|
+
const body = readSkillBody(name);
|
|
843
|
+
if (body === null)
|
|
844
|
+
throw new Error(`Skill not found: ${name}`);
|
|
845
|
+
console.log(body);
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
if (sub === "new") {
|
|
849
|
+
const name = positional[2];
|
|
850
|
+
if (!name)
|
|
851
|
+
throw new Error("Usage: amem skills new <name> [--desc <text>]");
|
|
852
|
+
const slug = slugifySkillName(name);
|
|
853
|
+
if (!isValidSkillName(slug))
|
|
854
|
+
throw new Error(`Invalid skill name: ${name}`);
|
|
855
|
+
if (findSkillOnDisk(slug))
|
|
856
|
+
throw new Error(`Skill already exists: ${slug}`);
|
|
857
|
+
const markdown = renderSkillMarkdown({
|
|
858
|
+
name: slug,
|
|
859
|
+
description: flagString(flags, "desc") || `Procedure: ${slug.replace(/[-_]+/g, " ")}`,
|
|
860
|
+
});
|
|
861
|
+
const written = writeSkill(slug, markdown);
|
|
862
|
+
syncSkillIndex();
|
|
863
|
+
console.log(`Created ${written.path}`);
|
|
864
|
+
console.log("Edit it, then agents will see it in their skill index.");
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
if (sub === "rm") {
|
|
868
|
+
const name = positional[2];
|
|
869
|
+
if (!name)
|
|
870
|
+
throw new Error("Usage: amem skills rm <name>");
|
|
871
|
+
if (!deleteSkill(name))
|
|
872
|
+
throw new Error(`Skill not found: ${name}`);
|
|
873
|
+
syncSkillIndex();
|
|
874
|
+
console.log(`Deleted skill ${slugifySkillName(name)}`);
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
if (sub === "sync") {
|
|
878
|
+
const skills = syncSkillIndex();
|
|
879
|
+
console.log(`Indexed ${skills.length} skill(s) from ${skillsDir()}`);
|
|
880
|
+
const edited = skills.filter((s) => s.modified);
|
|
881
|
+
if (edited.length > 0) {
|
|
882
|
+
console.log(`${edited.length} locally edited: ${edited.map((s) => s.name).join(", ")}`);
|
|
883
|
+
}
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
if (sub === "drafts") {
|
|
887
|
+
const drafts = listSkillDrafts({ status: "pending", limit: 50 });
|
|
888
|
+
if (drafts.length === 0) {
|
|
889
|
+
console.log("No pending skill drafts.");
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
for (const d of drafts) {
|
|
893
|
+
const label = d.kind === "revision" ? `revise ${d.target_skill}` : d.kind === "create" ? "staged" : "suggestion";
|
|
894
|
+
console.log(`${d.id} [${label}] ${d.title}`);
|
|
895
|
+
const why = JSON.parse(d.reasons || "[]");
|
|
896
|
+
if (Array.isArray(why) && why.length)
|
|
897
|
+
console.log(` why: ${why.join(", ")}`);
|
|
898
|
+
}
|
|
899
|
+
console.log(`\n${drafts.length} pending · approve with: amem skills approve <id>`);
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
if (sub === "approve" || sub === "dismiss") {
|
|
903
|
+
const id = positional[2];
|
|
904
|
+
if (!id)
|
|
905
|
+
throw new Error(`Usage: amem skills ${sub} <draft-id>`);
|
|
906
|
+
const path = sub === "approve" ? "/api/skills/drafts/apply" : "/api/skills/drafts/dismiss";
|
|
907
|
+
const result = handleApi({
|
|
908
|
+
method: "POST",
|
|
909
|
+
pathname: path,
|
|
910
|
+
searchParams: new URLSearchParams(),
|
|
911
|
+
body: { id },
|
|
912
|
+
cwd: process.cwd(),
|
|
913
|
+
});
|
|
914
|
+
if (result.status >= 400) {
|
|
915
|
+
throw new Error(String(result.body?.error || "Failed"));
|
|
916
|
+
}
|
|
917
|
+
console.log(sub === "approve" ? `Applied ${id}` : `Dismissed ${id}`);
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
if (sub === "import") {
|
|
921
|
+
const src = positional[2];
|
|
922
|
+
if (!src)
|
|
923
|
+
throw new Error("Usage: amem skills import <path-to-skill-dir>");
|
|
924
|
+
const result = importSkillFromPath(src, flagString(flags, "name"));
|
|
925
|
+
syncSkillIndex();
|
|
926
|
+
console.log(`Imported ${result.name} → ${result.path}`);
|
|
927
|
+
console.log("Review it before trusting: skills are instructions agents follow.");
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
throw new Error("Usage: amem skills list|show|new|rm|sync|import");
|
|
931
|
+
}
|
|
694
932
|
case "it-pack": {
|
|
695
933
|
const out = resolve(flagString(flags, "out") || join(amemHome(), "it-pack"));
|
|
696
934
|
const result = writeItPack(out);
|
|
@@ -919,7 +1157,8 @@ async function main() {
|
|
|
919
1157
|
break;
|
|
920
1158
|
}
|
|
921
1159
|
if (sub === "activate") {
|
|
922
|
-
|
|
1160
|
+
console.log("amem is 100% free and open — all features are already unlocked.");
|
|
1161
|
+
break;
|
|
923
1162
|
}
|
|
924
1163
|
if (sub === "clear") {
|
|
925
1164
|
clearLicense();
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type ClaimRow, type ComponentRow, type ConversationNoteRow, type FlowRow, type UsageEventRow } from "./db.js";
|
|
1
|
+
import { type AgentTaskRow, type ClaimRow, type ComponentRow, type ConversationNoteRow, type FlowRow, type UsageEventRow, type SkillDraftRow } from "./db.js";
|
|
2
2
|
import { type ClaimFreshness } from "./freshness.js";
|
|
3
|
+
import { type RankedSkill } from "./skills.js";
|
|
3
4
|
export type RankedClaim = ClaimRow & {
|
|
4
5
|
score: number;
|
|
5
6
|
freshness: ClaimFreshness;
|
|
@@ -14,12 +15,20 @@ export type ContextPacket = {
|
|
|
14
15
|
notes: Array<ConversationNoteRow & {
|
|
15
16
|
score: number;
|
|
16
17
|
}>;
|
|
18
|
+
/** Deferred Kanban tasks (non-done), compact for agents. */
|
|
19
|
+
tasks: AgentTaskRow[];
|
|
20
|
+
/** Index-only skill matches; bodies load on demand via `amem_skill_view`. */
|
|
21
|
+
skills: RankedSkill[];
|
|
22
|
+
/** At most one pending "worth writing up" nudge from a previous session. */
|
|
23
|
+
skillDrafts: SkillDraftRow[];
|
|
17
24
|
};
|
|
18
25
|
export type BuildContextOptions = {
|
|
19
26
|
limit?: number;
|
|
20
27
|
rootPath?: string;
|
|
21
28
|
/** Include cross-repo personal prefs (default true). */
|
|
22
29
|
includePersonal?: boolean;
|
|
30
|
+
/** Include the ranked skill index (default true). */
|
|
31
|
+
includeSkills?: boolean;
|
|
23
32
|
};
|
|
24
33
|
export declare function buildContext(repoId: string, query: string, limitOrOpts?: number | BuildContextOptions): ContextPacket;
|
|
25
34
|
export type ShowdownClaim = {
|
package/dist/context.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import { getRepoByName, listClaims, listComponents, listConversationNotes, listEdges, listFlows, openDb, } from "./db.js";
|
|
1
|
+
import { getRepoByName, listClaims, listComponents, listConversationNotes, listEdges, listFlows, listOpenTasksForContext, listTasks, openDb, listSkillDrafts, } from "./db.js";
|
|
2
2
|
import { assessClaimFreshness, freshnessScoreMultiplier, } from "./freshness.js";
|
|
3
3
|
import { kindRankBoost } from "./kinds.js";
|
|
4
4
|
import { ftsBoostFromBm25, keywordScoreClaim, searchClaimsFts, tokenize, } from "./search.js";
|
|
5
5
|
import { embedBoostFromScore, searchClaimsEmbed, searchClaimsEmbedLive } from "./embed.js";
|
|
6
6
|
import { FEATURE_LOCAL_EMBED, hasFeature } from "./license.js";
|
|
7
7
|
import { PERSONAL_SLUG } from "./personal.js";
|
|
8
|
+
import { listIndexedSkills, rankSkills } from "./skills.js";
|
|
9
|
+
import { loadPolicy } from "./policy.js";
|
|
10
|
+
/** Reasons are stored as JSON; a malformed row must not break the whole packet. */
|
|
11
|
+
function safeReasons(raw) {
|
|
12
|
+
try {
|
|
13
|
+
const list = JSON.parse(raw);
|
|
14
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
15
|
+
return ` (${list.filter((r) => typeof r === "string").join(", ")})`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* fall through */
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
8
23
|
function scoreNote(note, queryTokens) {
|
|
9
24
|
if (queryTokens.length === 0)
|
|
10
25
|
return 0;
|
|
@@ -16,6 +31,17 @@ function scoreNote(note, queryTokens) {
|
|
|
16
31
|
}
|
|
17
32
|
return score;
|
|
18
33
|
}
|
|
34
|
+
function scoreTask(task, queryTokens) {
|
|
35
|
+
if (queryTokens.length === 0)
|
|
36
|
+
return 0;
|
|
37
|
+
const hay = `${task.title} ${task.body} ${task.anchors || ""}`.toLowerCase();
|
|
38
|
+
let score = 0;
|
|
39
|
+
for (const token of queryTokens) {
|
|
40
|
+
if (hay.includes(token))
|
|
41
|
+
score += token.length > 4 ? 3 : 2;
|
|
42
|
+
}
|
|
43
|
+
return score;
|
|
44
|
+
}
|
|
19
45
|
function rankClaims(claims, query, queryTokens, opts) {
|
|
20
46
|
return claims.map((c) => {
|
|
21
47
|
const freshness = assessClaimFreshness(opts.rootPath, c);
|
|
@@ -152,7 +178,44 @@ export function buildContext(repoId, query, limitOrOpts = 12) {
|
|
|
152
178
|
const notes = notesRaw.length > 0
|
|
153
179
|
? notesRaw
|
|
154
180
|
: listConversationNotes(repoId, 5).map((n) => ({ ...n, score: 0 }));
|
|
155
|
-
|
|
181
|
+
const openTasks = listOpenTasksForContext(repoId, 8);
|
|
182
|
+
const doneTasks = queryTokens.length > 0
|
|
183
|
+
? listTasks(repoId, { status: "done", includeDone: true, limit: 20 })
|
|
184
|
+
.map((t) => ({ ...t, score: scoreTask(t, queryTokens) }))
|
|
185
|
+
.filter((t) => t.score > 0)
|
|
186
|
+
.sort((a, b) => b.score - a.score || b.updated_at.localeCompare(a.updated_at))
|
|
187
|
+
.slice(0, 4)
|
|
188
|
+
: [];
|
|
189
|
+
const tasks = [...openTasks, ...doneTasks];
|
|
190
|
+
const skillsOn = opts.includeSkills !== false && loadPolicy().policy.skills_enabled;
|
|
191
|
+
const skills = skillsOn ? rankSkillsForQuery(query) : [];
|
|
192
|
+
const skillDrafts = skillsOn ? pendingSkillNudges(repoId) : [];
|
|
193
|
+
return { query, claims: selected, flows, components, notes, tasks, skills, skillDrafts };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* amem has no model, so it cannot write a skill itself. The nudge is how a session-end
|
|
197
|
+
* detection reaches an agent that can. One at a time — this rides in every packet.
|
|
198
|
+
*/
|
|
199
|
+
function pendingSkillNudges(repoId) {
|
|
200
|
+
try {
|
|
201
|
+
return listSkillDrafts({ status: "pending", repoId, limit: 1 });
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Progressive disclosure, level 0. Only names and descriptions reach the packet; the
|
|
209
|
+
* agent pulls a body with `amem_skill_view` if it decides the procedure applies. A skills
|
|
210
|
+
* directory that cannot be read must never break context, so this stays best-effort.
|
|
211
|
+
*/
|
|
212
|
+
function rankSkillsForQuery(query) {
|
|
213
|
+
try {
|
|
214
|
+
return rankSkills(listIndexedSkills(), query, 3);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
156
219
|
}
|
|
157
220
|
function toShowdownClaims(ranked) {
|
|
158
221
|
return ranked.map((c) => ({
|
|
@@ -258,7 +321,11 @@ export function decorateUsageEvents(events) {
|
|
|
258
321
|
}
|
|
259
322
|
export function renderContextMarkdown(packet) {
|
|
260
323
|
const lines = ["# Agent Memory Context", "", `Query: ${packet.query}`, ""];
|
|
261
|
-
if (packet.claims.length === 0 &&
|
|
324
|
+
if (packet.claims.length === 0 &&
|
|
325
|
+
packet.notes.length === 0 &&
|
|
326
|
+
(packet.tasks?.length ?? 0) === 0 &&
|
|
327
|
+
(packet.skills?.length ?? 0) === 0 &&
|
|
328
|
+
(packet.skillDrafts?.length ?? 0) === 0) {
|
|
262
329
|
lines.push("No claims stored for this repository yet.", "");
|
|
263
330
|
lines.push("Seed memory with the `amem-bootstrap` skill or `amem propose apply <file>`.");
|
|
264
331
|
return lines.join("\n");
|
|
@@ -266,6 +333,41 @@ export function renderContextMarkdown(packet) {
|
|
|
266
333
|
if (packet.claims.length === 0) {
|
|
267
334
|
lines.push("No durable claims yet — using recent conversation memory.", "");
|
|
268
335
|
}
|
|
336
|
+
if (packet.tasks && packet.tasks.length > 0) {
|
|
337
|
+
const hasDone = packet.tasks.some((t) => t.status === "done");
|
|
338
|
+
const hasOpen = packet.tasks.some((t) => t.status !== "done");
|
|
339
|
+
const header = hasDone && !hasOpen ? "## Completed tasks" : (hasDone ? "## Tasks (open & completed)" : "## Open tasks");
|
|
340
|
+
lines.push(header, "");
|
|
341
|
+
lines.push("_Project Kanban tasks. Use `amem_task_*` to add/update/complete._", "");
|
|
342
|
+
for (const task of packet.tasks) {
|
|
343
|
+
const body = task.body ? ` — ${task.body.replace(/\s+/g, " ").slice(0, 120)}` : "";
|
|
344
|
+
lines.push(`- **[${task.status}]** ${task.title}${body} (\`${task.id}\`)`);
|
|
345
|
+
}
|
|
346
|
+
lines.push("");
|
|
347
|
+
}
|
|
348
|
+
if (packet.skills && packet.skills.length > 0) {
|
|
349
|
+
lines.push("## Relevant skills", "");
|
|
350
|
+
lines.push("_Procedures that may apply. Only the index is shown — call `amem_skill_view` with the name to load one before following it._", "");
|
|
351
|
+
for (const skill of packet.skills) {
|
|
352
|
+
const desc = skill.description
|
|
353
|
+
? ` — ${skill.description.replace(/\s+/g, " ").slice(0, 140)}`
|
|
354
|
+
: "";
|
|
355
|
+
lines.push(`- **${skill.name}**${desc}`);
|
|
356
|
+
}
|
|
357
|
+
lines.push("");
|
|
358
|
+
}
|
|
359
|
+
const nudge = packet.skillDrafts?.[0];
|
|
360
|
+
if (nudge) {
|
|
361
|
+
const why = safeReasons(nudge.reasons);
|
|
362
|
+
if (nudge.kind === "revision" && nudge.target_skill) {
|
|
363
|
+
lines.push("## Skill worth revising", "");
|
|
364
|
+
lines.push(`_You followed **${nudge.target_skill}** in a recent session and the work still went sideways${why}. If you learned what it was missing, update it with \`amem_skill_save\`._`, "");
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
lines.push("## Worth saving as a skill", "");
|
|
368
|
+
lines.push(`_A recent session looked like a repeatable procedure: "${nudge.title}"${why}. If you can write it up, save it with \`amem_skill_save\` so the next agent does not rediscover it._`, "");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
269
371
|
const staleCount = packet.claims.filter((c) => c.freshness.status === "stale").length;
|
|
270
372
|
if (staleCount > 0) {
|
|
271
373
|
lines.push(`_${staleCount} claim(s) marked stale — anchored files changed after the claim was written. Verify before trusting._`, "");
|
package/dist/db.d.ts
CHANGED
|
@@ -96,6 +96,21 @@ export type SetupStateRow = {
|
|
|
96
96
|
setup_completed_at: string | null;
|
|
97
97
|
updated_at: string;
|
|
98
98
|
};
|
|
99
|
+
export type AgentTaskStatus = "backlog" | "next" | "doing" | "blocked" | "done";
|
|
100
|
+
export type AgentTaskRow = {
|
|
101
|
+
repo_id: string;
|
|
102
|
+
id: string;
|
|
103
|
+
title: string;
|
|
104
|
+
body: string;
|
|
105
|
+
status: AgentTaskStatus;
|
|
106
|
+
anchors: string;
|
|
107
|
+
source: string;
|
|
108
|
+
created_at: string;
|
|
109
|
+
updated_at: string;
|
|
110
|
+
completed_at: string | null;
|
|
111
|
+
};
|
|
112
|
+
export declare const AGENT_TASK_STATUSES: readonly AgentTaskStatus[];
|
|
113
|
+
export declare function normalizeTaskStatus(raw: unknown): AgentTaskStatus | null;
|
|
99
114
|
export declare function openDb(): Database.Database;
|
|
100
115
|
export declare function closeDb(): void;
|
|
101
116
|
declare function nowIso(): string;
|
|
@@ -189,4 +204,130 @@ export declare function listProposalDraftsAll(opts?: {
|
|
|
189
204
|
export declare function countProposalDrafts(repoId: string, status?: string): number;
|
|
190
205
|
export declare function countProposalDraftsAll(status?: string): number;
|
|
191
206
|
export declare function setProposalDraftStatus(id: string, status: "pending" | "applied" | "dismissed"): ProposalDraftRow | null;
|
|
207
|
+
export type SkillRow = {
|
|
208
|
+
name: string;
|
|
209
|
+
path: string;
|
|
210
|
+
description: string;
|
|
211
|
+
version: string | null;
|
|
212
|
+
tags: string;
|
|
213
|
+
repo_id: string | null;
|
|
214
|
+
content_hash: string;
|
|
215
|
+
origin_hash: string | null;
|
|
216
|
+
source: string;
|
|
217
|
+
uses: number;
|
|
218
|
+
last_used_at: string | null;
|
|
219
|
+
created_at: string;
|
|
220
|
+
updated_at: string;
|
|
221
|
+
};
|
|
222
|
+
export declare function listSkillRows(): SkillRow[];
|
|
223
|
+
export declare function getSkillRow(name: string): SkillRow | null;
|
|
224
|
+
/**
|
|
225
|
+
* Index one skill found on disk. Disk is the source of truth, so this only ever refreshes
|
|
226
|
+
* derived columns — it must not clobber the repo tag or usage counters a user built up.
|
|
227
|
+
*/
|
|
228
|
+
export declare function upsertSkillRow(input: {
|
|
229
|
+
name: string;
|
|
230
|
+
path: string;
|
|
231
|
+
description?: string;
|
|
232
|
+
version?: string | null;
|
|
233
|
+
tags?: string[];
|
|
234
|
+
contentHash: string;
|
|
235
|
+
source?: string;
|
|
236
|
+
repoId?: string | null;
|
|
237
|
+
}): SkillRow;
|
|
238
|
+
/** Optional memory tag. Skills are a global library; the tag is only a filter hint. */
|
|
239
|
+
export declare function setSkillRepo(name: string, repoId: string | null): void;
|
|
240
|
+
export declare function deleteSkillRow(name: string): boolean;
|
|
241
|
+
/** Drop index rows whose skill is no longer on disk. */
|
|
242
|
+
export declare function pruneSkillRows(keepNames: string[]): number;
|
|
243
|
+
export declare function recordSkillUse(name: string, ctx?: {
|
|
244
|
+
repoId?: string | null;
|
|
245
|
+
sessionId?: string | null;
|
|
246
|
+
}): void;
|
|
247
|
+
/**
|
|
248
|
+
* Skills used recently in a repo. MCP clients do not always carry a session id, so
|
|
249
|
+
* recency in the same memory is the fallback for correlating a view to a session.
|
|
250
|
+
*/
|
|
251
|
+
export declare function listRecentSkillUses(repoId: string, minutes?: number, limit?: number): string[];
|
|
252
|
+
export declare function listSkillsUsedInSession(sessionId: string, limit?: number): string[];
|
|
253
|
+
export type SkillDraftRow = {
|
|
254
|
+
id: string;
|
|
255
|
+
repo_id: string | null;
|
|
256
|
+
name: string | null;
|
|
257
|
+
title: string;
|
|
258
|
+
summary: string;
|
|
259
|
+
content: string | null;
|
|
260
|
+
kind: string;
|
|
261
|
+
target_skill: string | null;
|
|
262
|
+
status: string;
|
|
263
|
+
source: string;
|
|
264
|
+
session_id: string | null;
|
|
265
|
+
reasons: string;
|
|
266
|
+
created_at: string;
|
|
267
|
+
updated_at: string;
|
|
268
|
+
};
|
|
269
|
+
export declare function insertSkillDraft(input: {
|
|
270
|
+
repoId?: string | null;
|
|
271
|
+
name?: string | null;
|
|
272
|
+
title: string;
|
|
273
|
+
summary?: string;
|
|
274
|
+
content?: string | null;
|
|
275
|
+
kind?: "suggestion" | "create" | "revision";
|
|
276
|
+
targetSkill?: string | null;
|
|
277
|
+
source?: string;
|
|
278
|
+
sessionId?: string | null;
|
|
279
|
+
reasons?: string[];
|
|
280
|
+
}): SkillDraftRow;
|
|
281
|
+
export declare function getSkillDraft(id: string): SkillDraftRow | null;
|
|
282
|
+
export declare function listSkillDrafts(opts?: {
|
|
283
|
+
status?: string;
|
|
284
|
+
repoId?: string;
|
|
285
|
+
limit?: number;
|
|
286
|
+
}): SkillDraftRow[];
|
|
287
|
+
export declare function setSkillDraftStatus(id: string, status: string): SkillDraftRow | null;
|
|
288
|
+
export declare function skillDraftExists(source: string): boolean;
|
|
289
|
+
export declare function getTask(repoId: string, id: string): AgentTaskRow | null;
|
|
290
|
+
export declare function listTasks(repoId: string, opts?: {
|
|
291
|
+
status?: AgentTaskStatus;
|
|
292
|
+
includeDone?: boolean;
|
|
293
|
+
limit?: number;
|
|
294
|
+
}): AgentTaskRow[];
|
|
295
|
+
/**
|
|
296
|
+
* Tasks across every memory. The UI's "All memory" scope needs this because agents file
|
|
297
|
+
* tasks against whatever repo they were working in, which is often not the repo the UI
|
|
298
|
+
* was launched from — without it those tasks are invisible.
|
|
299
|
+
*/
|
|
300
|
+
export declare function listTasksAll(opts?: {
|
|
301
|
+
status?: AgentTaskStatus;
|
|
302
|
+
includeDone?: boolean;
|
|
303
|
+
limit?: number;
|
|
304
|
+
}): AgentTaskRow[];
|
|
305
|
+
export declare function countTasksAll(opts?: {
|
|
306
|
+
status?: AgentTaskStatus;
|
|
307
|
+
openOnly?: boolean;
|
|
308
|
+
}): number;
|
|
309
|
+
/** Find a task without knowing its repo, so all-memory edits can resolve their owner. */
|
|
310
|
+
export declare function findTaskAnyRepo(id: string): AgentTaskRow | null;
|
|
311
|
+
/** Open tasks for context injection — prefer doing/next/blocked, then backlog. */
|
|
312
|
+
export declare function listOpenTasksForContext(repoId: string, limit?: number): AgentTaskRow[];
|
|
313
|
+
export declare function countTasks(repoId: string, opts?: {
|
|
314
|
+
status?: AgentTaskStatus;
|
|
315
|
+
openOnly?: boolean;
|
|
316
|
+
}): number;
|
|
317
|
+
export declare function insertTask(input: {
|
|
318
|
+
repoId: string;
|
|
319
|
+
title: string;
|
|
320
|
+
body?: string;
|
|
321
|
+
status?: AgentTaskStatus | string;
|
|
322
|
+
anchors?: string[];
|
|
323
|
+
source?: string;
|
|
324
|
+
}): AgentTaskRow;
|
|
325
|
+
export declare function updateTask(repoId: string, id: string, patch: {
|
|
326
|
+
title?: string;
|
|
327
|
+
body?: string;
|
|
328
|
+
status?: AgentTaskStatus | string;
|
|
329
|
+
anchors?: string[];
|
|
330
|
+
}): AgentTaskRow | null;
|
|
331
|
+
export declare function completeTask(repoId: string, id: string): AgentTaskRow | null;
|
|
332
|
+
export declare function deleteTask(repoId: string, id: string): boolean;
|
|
192
333
|
export { nowIso };
|