@aman_asmuei/aman-agent 0.25.0 → 0.27.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/README.md +26 -14
- package/dist/index.js +1060 -204
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1387,18 +1387,18 @@ var McpManager = class {
|
|
|
1387
1387
|
|
|
1388
1388
|
// src/agent.ts
|
|
1389
1389
|
import * as readline from "readline";
|
|
1390
|
-
import
|
|
1391
|
-
import
|
|
1392
|
-
import
|
|
1390
|
+
import fs20 from "fs";
|
|
1391
|
+
import path20 from "path";
|
|
1392
|
+
import os19 from "os";
|
|
1393
1393
|
import pc7 from "picocolors";
|
|
1394
1394
|
import { marked } from "marked";
|
|
1395
1395
|
import { markedTerminal } from "marked-terminal";
|
|
1396
1396
|
import logUpdate from "log-update";
|
|
1397
1397
|
|
|
1398
1398
|
// src/commands.ts
|
|
1399
|
-
import
|
|
1400
|
-
import
|
|
1401
|
-
import
|
|
1399
|
+
import fs17 from "fs";
|
|
1400
|
+
import path17 from "path";
|
|
1401
|
+
import os16 from "os";
|
|
1402
1402
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1403
1403
|
import pc5 from "picocolors";
|
|
1404
1404
|
|
|
@@ -2361,8 +2361,9 @@ import pc3 from "picocolors";
|
|
|
2361
2361
|
// src/hooks.ts
|
|
2362
2362
|
import pc2 from "picocolors";
|
|
2363
2363
|
import * as p2 from "@clack/prompts";
|
|
2364
|
-
import
|
|
2365
|
-
import
|
|
2364
|
+
import fs14 from "fs";
|
|
2365
|
+
import path14 from "path";
|
|
2366
|
+
import os13 from "os";
|
|
2366
2367
|
|
|
2367
2368
|
// src/personality.ts
|
|
2368
2369
|
var FRUSTRATION_SIGNALS = [
|
|
@@ -2523,13 +2524,19 @@ function formatWellbeingNudge(state) {
|
|
|
2523
2524
|
if (!state.wellbeingNudge) return null;
|
|
2524
2525
|
return WELLBEING_NUDGES[state.wellbeingNudge] || null;
|
|
2525
2526
|
}
|
|
2526
|
-
async function syncPersonalityToCore(state, mcpManager) {
|
|
2527
|
+
async function syncPersonalityToCore(state, mcpManager, modelMetrics) {
|
|
2527
2528
|
try {
|
|
2528
|
-
|
|
2529
|
+
const payload = {
|
|
2529
2530
|
currentRead: state.currentRead,
|
|
2530
2531
|
energy: state.energy,
|
|
2531
2532
|
activeMode: state.activeMode
|
|
2532
|
-
}
|
|
2533
|
+
};
|
|
2534
|
+
if (modelMetrics) {
|
|
2535
|
+
payload.trust = `${(modelMetrics.trustScore * 100).toFixed(0)}%`;
|
|
2536
|
+
payload.sessions = modelMetrics.totalSessions;
|
|
2537
|
+
payload.sentimentTrend = modelMetrics.sentimentTrend;
|
|
2538
|
+
}
|
|
2539
|
+
await mcpManager.callTool("identity_update_dynamics", payload);
|
|
2533
2540
|
} catch (err) {
|
|
2534
2541
|
log.debug("personality", "identity_update_dynamics failed", err);
|
|
2535
2542
|
}
|
|
@@ -2689,8 +2696,28 @@ Return ONLY valid JSON matching this schema (no markdown, no explanation):
|
|
|
2689
2696
|
"decisions": ["key choices made with rationale"],
|
|
2690
2697
|
"sentimentArc": "how mood evolved during session",
|
|
2691
2698
|
"patterns": ["recurring behaviors worth remembering for future sessions"],
|
|
2692
|
-
"recommendations": ["actionable suggestions for next session"]
|
|
2693
|
-
|
|
2699
|
+
"recommendations": ["actionable suggestions for next session"],
|
|
2700
|
+
"crystallizationCandidates": [
|
|
2701
|
+
{
|
|
2702
|
+
"name": "lowercase-kebab-name",
|
|
2703
|
+
"description": "1-sentence description of when this would be useful",
|
|
2704
|
+
"triggers": ["3-8", "trigger", "keywords"],
|
|
2705
|
+
"approach": "1-paragraph context: when and why to use this procedure",
|
|
2706
|
+
"steps": ["ordered step 1", "ordered step 2"],
|
|
2707
|
+
"gotchas": ["common mistake 1"],
|
|
2708
|
+
"confidence": 0.0
|
|
2709
|
+
}
|
|
2710
|
+
]
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
CRYSTALLIZATION RULES:
|
|
2714
|
+
- Only suggest 0-2 candidates per session \u2014 if nothing qualifies, return an empty array
|
|
2715
|
+
- Only suggest REUSABLE procedures (not one-off tasks specific to today's work)
|
|
2716
|
+
- The user must have demonstrated the procedure in this session
|
|
2717
|
+
- Confidence < 0.6 \u2192 don't suggest at all
|
|
2718
|
+
- Skip vague things like "use library X" \u2014 that's not procedural knowledge
|
|
2719
|
+
- Prefer narrow specific procedures over broad generalizations
|
|
2720
|
+
- Trigger keywords should be highly specific (avoid generic words like "code", "fix", "the")`;
|
|
2694
2721
|
async function generatePostmortemReport(sessionId, messages, session, client, obsDir) {
|
|
2695
2722
|
try {
|
|
2696
2723
|
const events = await readObservationEvents(sessionId, obsDir ?? defaultObservationsDir2());
|
|
@@ -2770,7 +2797,8 @@ ${obsSnapshot.join("\n")}`;
|
|
|
2770
2797
|
topicProgression: [...new Set(topicProgression)],
|
|
2771
2798
|
sentimentArc: parsed.sentimentArc ?? "",
|
|
2772
2799
|
patterns: parsed.patterns ?? [],
|
|
2773
|
-
recommendations: parsed.recommendations ?? []
|
|
2800
|
+
recommendations: parsed.recommendations ?? [],
|
|
2801
|
+
crystallizationCandidates: Array.isArray(parsed.crystallizationCandidates) ? parsed.crystallizationCandidates : void 0
|
|
2774
2802
|
};
|
|
2775
2803
|
} catch (err) {
|
|
2776
2804
|
log.debug("postmortem", "Failed to generate post-mortem", err);
|
|
@@ -2841,6 +2869,14 @@ function formatPostmortemMarkdown(report) {
|
|
|
2841
2869
|
report.recommendations.forEach((r) => lines.push(`- ${r}`));
|
|
2842
2870
|
lines.push("");
|
|
2843
2871
|
}
|
|
2872
|
+
if (report.crystallizationCandidates && report.crystallizationCandidates.length > 0) {
|
|
2873
|
+
lines.push("## Crystallization Candidates");
|
|
2874
|
+
report.crystallizationCandidates.forEach((c) => {
|
|
2875
|
+
lines.push(`- **${c.name}** (confidence ${c.confidence})`);
|
|
2876
|
+
lines.push(` ${c.description}`);
|
|
2877
|
+
});
|
|
2878
|
+
lines.push("");
|
|
2879
|
+
}
|
|
2844
2880
|
return lines.join("\n");
|
|
2845
2881
|
}
|
|
2846
2882
|
async function savePostmortem(report, dir) {
|
|
@@ -2851,6 +2887,12 @@ async function savePostmortem(report, dir) {
|
|
|
2851
2887
|
const filePath = path11.join(pmDir, fileName);
|
|
2852
2888
|
const markdown = formatPostmortemMarkdown(report);
|
|
2853
2889
|
await fs11.writeFile(filePath, markdown, "utf-8");
|
|
2890
|
+
const jsonPath = filePath.replace(/\.md$/, ".json");
|
|
2891
|
+
try {
|
|
2892
|
+
await fs11.writeFile(jsonPath, JSON.stringify(report, null, 2), "utf-8");
|
|
2893
|
+
} catch (err) {
|
|
2894
|
+
log.debug("postmortem", "JSON sidecar write failed", err);
|
|
2895
|
+
}
|
|
2854
2896
|
return filePath;
|
|
2855
2897
|
}
|
|
2856
2898
|
async function listPostmortems(dir) {
|
|
@@ -2911,6 +2953,511 @@ ${contents.join("\n\n---\n\n")}`
|
|
|
2911
2953
|
}
|
|
2912
2954
|
}
|
|
2913
2955
|
|
|
2956
|
+
// src/crystallization.ts
|
|
2957
|
+
import fs12 from "fs/promises";
|
|
2958
|
+
import path12 from "path";
|
|
2959
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
2960
|
+
"the",
|
|
2961
|
+
"and",
|
|
2962
|
+
"is",
|
|
2963
|
+
"to",
|
|
2964
|
+
"of",
|
|
2965
|
+
"a",
|
|
2966
|
+
"in",
|
|
2967
|
+
"for",
|
|
2968
|
+
"on",
|
|
2969
|
+
"with",
|
|
2970
|
+
"this",
|
|
2971
|
+
"that",
|
|
2972
|
+
"it",
|
|
2973
|
+
"as",
|
|
2974
|
+
"be",
|
|
2975
|
+
"by",
|
|
2976
|
+
"or",
|
|
2977
|
+
"at",
|
|
2978
|
+
"an",
|
|
2979
|
+
"from",
|
|
2980
|
+
"code",
|
|
2981
|
+
"fix",
|
|
2982
|
+
"do",
|
|
2983
|
+
"use",
|
|
2984
|
+
"make",
|
|
2985
|
+
"get",
|
|
2986
|
+
"set",
|
|
2987
|
+
"run",
|
|
2988
|
+
"we",
|
|
2989
|
+
"i"
|
|
2990
|
+
]);
|
|
2991
|
+
var MAX_REJECTIONS = 100;
|
|
2992
|
+
var MARKER_RE = /<!--\s*aman-auto\s+([^>]+?)\s*-->/;
|
|
2993
|
+
function sanitizeName(input) {
|
|
2994
|
+
const cleaned = input.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2995
|
+
if (cleaned.length === 0) {
|
|
2996
|
+
throw new Error(`Cannot sanitize name: "${input}" produced empty result`);
|
|
2997
|
+
}
|
|
2998
|
+
return cleaned;
|
|
2999
|
+
}
|
|
3000
|
+
function validateCandidate(raw) {
|
|
3001
|
+
if (!raw || typeof raw !== "object") return null;
|
|
3002
|
+
const c = raw;
|
|
3003
|
+
if (typeof c.name !== "string" || c.name.trim() === "") return null;
|
|
3004
|
+
if (typeof c.description !== "string") return null;
|
|
3005
|
+
if (typeof c.approach !== "string") return null;
|
|
3006
|
+
if (!Array.isArray(c.triggers) || c.triggers.length === 0) return null;
|
|
3007
|
+
if (c.triggers.length > 10) return null;
|
|
3008
|
+
if (!Array.isArray(c.steps)) return null;
|
|
3009
|
+
if (typeof c.confidence !== "number") return null;
|
|
3010
|
+
if (!Number.isFinite(c.confidence)) return null;
|
|
3011
|
+
if (c.confidence < 0.6) return null;
|
|
3012
|
+
const triggers = Array.from(
|
|
3013
|
+
new Set(
|
|
3014
|
+
c.triggers.filter((t) => typeof t === "string").map((t) => t.toLowerCase().trim()).filter((t) => t.length > 0 && !STOPWORDS.has(t))
|
|
3015
|
+
)
|
|
3016
|
+
);
|
|
3017
|
+
if (triggers.length === 0) return null;
|
|
3018
|
+
let name;
|
|
3019
|
+
try {
|
|
3020
|
+
name = sanitizeName(c.name);
|
|
3021
|
+
} catch {
|
|
3022
|
+
return null;
|
|
3023
|
+
}
|
|
3024
|
+
return {
|
|
3025
|
+
name,
|
|
3026
|
+
description: c.description,
|
|
3027
|
+
triggers,
|
|
3028
|
+
approach: c.approach,
|
|
3029
|
+
steps: c.steps.filter((s) => typeof s === "string"),
|
|
3030
|
+
gotchas: Array.isArray(c.gotchas) ? c.gotchas.filter((g) => typeof g === "string") : [],
|
|
3031
|
+
confidence: Math.min(1, Math.max(0, c.confidence))
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
function toTitleCase(kebab) {
|
|
3035
|
+
return kebab.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
3036
|
+
}
|
|
3037
|
+
function formatSkillMarkdown(candidate, postmortemFilename) {
|
|
3038
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3039
|
+
const heading = toTitleCase(candidate.name);
|
|
3040
|
+
const triggerStr = candidate.triggers.join(",");
|
|
3041
|
+
const lines = [
|
|
3042
|
+
`# ${heading}`,
|
|
3043
|
+
`<!-- aman-auto source=postmortem date=${date} confidence=${candidate.confidence} triggers="${triggerStr}" -->`,
|
|
3044
|
+
"",
|
|
3045
|
+
"## When to use",
|
|
3046
|
+
candidate.approach,
|
|
3047
|
+
"",
|
|
3048
|
+
"## Steps",
|
|
3049
|
+
...candidate.steps.map((s, i) => `${i + 1}. ${s}`),
|
|
3050
|
+
""
|
|
3051
|
+
];
|
|
3052
|
+
if (candidate.gotchas.length > 0) {
|
|
3053
|
+
lines.push("## Gotchas");
|
|
3054
|
+
lines.push(...candidate.gotchas.map((g) => `- ${g}`));
|
|
3055
|
+
lines.push("");
|
|
3056
|
+
}
|
|
3057
|
+
lines.push(`<!-- generated from ${postmortemFilename} -->`);
|
|
3058
|
+
lines.push("");
|
|
3059
|
+
return lines.join("\n");
|
|
3060
|
+
}
|
|
3061
|
+
function parseMarkerComment(line) {
|
|
3062
|
+
const match = line.match(MARKER_RE);
|
|
3063
|
+
if (!match) return null;
|
|
3064
|
+
const attrs = {};
|
|
3065
|
+
const attrRe = /(\w+)=(?:"([^"]*)"|(\S+))/g;
|
|
3066
|
+
let m;
|
|
3067
|
+
while ((m = attrRe.exec(match[1])) !== null) {
|
|
3068
|
+
attrs[m[1]] = m[2] ?? m[3] ?? "";
|
|
3069
|
+
}
|
|
3070
|
+
if (!attrs.triggers) return null;
|
|
3071
|
+
const triggers = attrs.triggers.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
3072
|
+
if (triggers.length === 0) return null;
|
|
3073
|
+
return {
|
|
3074
|
+
source: attrs.source ?? "unknown",
|
|
3075
|
+
date: attrs.date ?? "",
|
|
3076
|
+
confidence: attrs.confidence ? Number(attrs.confidence) : 0,
|
|
3077
|
+
triggers
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
function extractSkillsWithMarkers(skillsMdContent) {
|
|
3081
|
+
const result = /* @__PURE__ */ new Map();
|
|
3082
|
+
const lines = skillsMdContent.split("\n");
|
|
3083
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3084
|
+
const line = lines[i];
|
|
3085
|
+
if (line.startsWith("# ") && i + 1 < lines.length) {
|
|
3086
|
+
const headingText = line.slice(2).trim();
|
|
3087
|
+
const nextLine = lines[i + 1];
|
|
3088
|
+
const marker = parseMarkerComment(nextLine);
|
|
3089
|
+
if (marker) {
|
|
3090
|
+
try {
|
|
3091
|
+
const skillName = sanitizeName(headingText);
|
|
3092
|
+
result.set(skillName, marker);
|
|
3093
|
+
} catch {
|
|
3094
|
+
log.debug("crystallization", `cannot sanitize heading: ${headingText}`);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
return result;
|
|
3100
|
+
}
|
|
3101
|
+
function findCollision(name, triggers, existing) {
|
|
3102
|
+
if (existing.has(name)) {
|
|
3103
|
+
return { collides: true, collidesWith: name, reason: "exact name match" };
|
|
3104
|
+
}
|
|
3105
|
+
const triggerSet = new Set(triggers);
|
|
3106
|
+
for (const [otherName, otherData] of existing) {
|
|
3107
|
+
const otherTriggers = new Set(otherData.triggers);
|
|
3108
|
+
const intersection = [...triggerSet].filter((t) => otherTriggers.has(t)).length;
|
|
3109
|
+
const union = (/* @__PURE__ */ new Set([...triggerSet, ...otherTriggers])).size;
|
|
3110
|
+
const overlap = union > 0 ? intersection / union : 0;
|
|
3111
|
+
if (overlap >= 0.8) {
|
|
3112
|
+
return {
|
|
3113
|
+
collides: true,
|
|
3114
|
+
collidesWith: otherName,
|
|
3115
|
+
reason: `${Math.round(overlap * 100)}% trigger overlap`
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
return { collides: false };
|
|
3120
|
+
}
|
|
3121
|
+
async function writeSkillToFile(candidate, skillsMdPath, postmortemFilename) {
|
|
3122
|
+
try {
|
|
3123
|
+
await fs12.mkdir(path12.dirname(skillsMdPath), { recursive: true });
|
|
3124
|
+
let existingContent = "";
|
|
3125
|
+
try {
|
|
3126
|
+
existingContent = await fs12.readFile(skillsMdPath, "utf-8");
|
|
3127
|
+
} catch {
|
|
3128
|
+
existingContent = "# Skills\n\n";
|
|
3129
|
+
}
|
|
3130
|
+
if (existingContent.trim() === "") {
|
|
3131
|
+
existingContent = "# Skills\n\n";
|
|
3132
|
+
}
|
|
3133
|
+
const existingSkills = extractSkillsWithMarkers(existingContent);
|
|
3134
|
+
const collision = findCollision(candidate.name, candidate.triggers, existingSkills);
|
|
3135
|
+
if (collision.collides) {
|
|
3136
|
+
log.debug("crystallization", `collision detected: ${collision.reason}`);
|
|
3137
|
+
return {
|
|
3138
|
+
written: false,
|
|
3139
|
+
filePath: skillsMdPath,
|
|
3140
|
+
skillName: candidate.name,
|
|
3141
|
+
reason: `collision with "${collision.collidesWith}" (${collision.reason})`
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
const skillMarkdown = formatSkillMarkdown(candidate, postmortemFilename);
|
|
3145
|
+
const separator = existingContent.endsWith("\n\n") ? "" : existingContent.endsWith("\n") ? "\n" : "\n\n";
|
|
3146
|
+
await fs12.writeFile(
|
|
3147
|
+
skillsMdPath,
|
|
3148
|
+
existingContent + separator + skillMarkdown,
|
|
3149
|
+
"utf-8"
|
|
3150
|
+
);
|
|
3151
|
+
return {
|
|
3152
|
+
written: true,
|
|
3153
|
+
filePath: skillsMdPath,
|
|
3154
|
+
skillName: candidate.name
|
|
3155
|
+
};
|
|
3156
|
+
} catch (err) {
|
|
3157
|
+
log.warn("crystallization", "writeSkillToFile failed", err);
|
|
3158
|
+
return {
|
|
3159
|
+
written: false,
|
|
3160
|
+
filePath: skillsMdPath,
|
|
3161
|
+
skillName: candidate.name,
|
|
3162
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
async function appendCrystallizationLog(entry, logPath) {
|
|
3167
|
+
try {
|
|
3168
|
+
await fs12.mkdir(path12.dirname(logPath), { recursive: true });
|
|
3169
|
+
let existing = [];
|
|
3170
|
+
try {
|
|
3171
|
+
const content = await fs12.readFile(logPath, "utf-8");
|
|
3172
|
+
existing = JSON.parse(content);
|
|
3173
|
+
if (!Array.isArray(existing)) existing = [];
|
|
3174
|
+
} catch {
|
|
3175
|
+
existing = [];
|
|
3176
|
+
}
|
|
3177
|
+
existing.push(entry);
|
|
3178
|
+
await fs12.writeFile(logPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
3179
|
+
} catch (err) {
|
|
3180
|
+
log.debug("crystallization", "appendCrystallizationLog failed", err);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
async function appendRejection(candidate, postmortemFilename, rejectionsPath) {
|
|
3184
|
+
try {
|
|
3185
|
+
await fs12.mkdir(path12.dirname(rejectionsPath), { recursive: true });
|
|
3186
|
+
let existing = [];
|
|
3187
|
+
try {
|
|
3188
|
+
const content = await fs12.readFile(rejectionsPath, "utf-8");
|
|
3189
|
+
existing = JSON.parse(content);
|
|
3190
|
+
if (!Array.isArray(existing)) existing = [];
|
|
3191
|
+
} catch {
|
|
3192
|
+
existing = [];
|
|
3193
|
+
}
|
|
3194
|
+
existing.push({
|
|
3195
|
+
name: candidate.name,
|
|
3196
|
+
rejectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3197
|
+
fromPostmortem: postmortemFilename,
|
|
3198
|
+
triggers: candidate.triggers
|
|
3199
|
+
});
|
|
3200
|
+
while (existing.length > MAX_REJECTIONS) {
|
|
3201
|
+
existing.shift();
|
|
3202
|
+
}
|
|
3203
|
+
await fs12.writeFile(rejectionsPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
3204
|
+
} catch (err) {
|
|
3205
|
+
log.debug("crystallization", "appendRejection failed", err);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// src/user-model.ts
|
|
3210
|
+
import fs13 from "fs/promises";
|
|
3211
|
+
import path13 from "path";
|
|
3212
|
+
import os12 from "os";
|
|
3213
|
+
var MAX_SESSIONS = 30;
|
|
3214
|
+
var TRUST_ALPHA = 0.3;
|
|
3215
|
+
var MIN_SESSIONS_FOR_FEED_FORWARD = 5;
|
|
3216
|
+
var MIN_SESSIONS_FOR_CORRELATIONS = 10;
|
|
3217
|
+
function defaultModelPath() {
|
|
3218
|
+
return path13.join(os12.homedir(), ".acore", "user-model.json");
|
|
3219
|
+
}
|
|
3220
|
+
function createEmptyModel() {
|
|
3221
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3222
|
+
return {
|
|
3223
|
+
version: 1,
|
|
3224
|
+
sessions: [],
|
|
3225
|
+
profile: emptyProfile(),
|
|
3226
|
+
createdAt: now,
|
|
3227
|
+
updatedAt: now
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
function emptyProfile() {
|
|
3231
|
+
return {
|
|
3232
|
+
trustScore: 0.5,
|
|
3233
|
+
trustTrajectory: "stable",
|
|
3234
|
+
totalSessions: 0,
|
|
3235
|
+
preferredTimePeriod: "afternoon",
|
|
3236
|
+
energyDistribution: {},
|
|
3237
|
+
avgSessionMinutes: 0,
|
|
3238
|
+
baselineFrustration: 0,
|
|
3239
|
+
baselineExcitement: 0,
|
|
3240
|
+
sentimentTrend: "stable",
|
|
3241
|
+
frustrationCorrelations: { toolErrors: 0, longSessions: 0, lateNight: 0 },
|
|
3242
|
+
avgTurnsPerSession: 0,
|
|
3243
|
+
engagementTrend: "stable",
|
|
3244
|
+
nudgeStats: {}
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
async function loadUserModel(filePath) {
|
|
3248
|
+
const fp = filePath ?? defaultModelPath();
|
|
3249
|
+
try {
|
|
3250
|
+
const raw = await fs13.readFile(fp, "utf-8");
|
|
3251
|
+
const parsed = JSON.parse(raw);
|
|
3252
|
+
if (parsed?.version !== 1) return null;
|
|
3253
|
+
return parsed;
|
|
3254
|
+
} catch {
|
|
3255
|
+
return null;
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
async function saveUserModel(model, filePath) {
|
|
3259
|
+
const fp = filePath ?? defaultModelPath();
|
|
3260
|
+
const dir = path13.dirname(fp);
|
|
3261
|
+
await fs13.mkdir(dir, { recursive: true });
|
|
3262
|
+
const tmp = fp + `.tmp-${Date.now()}`;
|
|
3263
|
+
await fs13.writeFile(tmp, JSON.stringify(model, null, 2), "utf-8");
|
|
3264
|
+
await fs13.rename(tmp, fp);
|
|
3265
|
+
}
|
|
3266
|
+
function aggregateSession(model, snapshot) {
|
|
3267
|
+
const sessions = [...model.sessions, snapshot];
|
|
3268
|
+
while (sessions.length > MAX_SESSIONS) {
|
|
3269
|
+
sessions.shift();
|
|
3270
|
+
}
|
|
3271
|
+
const totalSessions = model.profile.totalSessions + 1;
|
|
3272
|
+
const profile = computeProfile(sessions, totalSessions);
|
|
3273
|
+
return {
|
|
3274
|
+
...model,
|
|
3275
|
+
sessions,
|
|
3276
|
+
profile,
|
|
3277
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
function computeProfile(sessions, totalSessions) {
|
|
3281
|
+
if (sessions.length === 0) return { ...emptyProfile(), totalSessions };
|
|
3282
|
+
const n = sessions.length;
|
|
3283
|
+
let trustScore = 0.5;
|
|
3284
|
+
for (const s of sessions) {
|
|
3285
|
+
trustScore = TRUST_ALPHA * ratingSignal(s) + (1 - TRUST_ALPHA) * trustScore;
|
|
3286
|
+
}
|
|
3287
|
+
const trustTrajectory = computeTrustTrajectory(sessions);
|
|
3288
|
+
const baselineFrustration = avg(sessions.map((s) => s.avgFrustration));
|
|
3289
|
+
const baselineExcitement = avg(sessions.map((s) => s.avgExcitement));
|
|
3290
|
+
const sentimentTrend = computeSentimentTrend(sessions);
|
|
3291
|
+
const energyDistribution = {};
|
|
3292
|
+
for (const s of sessions) {
|
|
3293
|
+
energyDistribution[s.timePeriod] = (energyDistribution[s.timePeriod] || 0) + 1;
|
|
3294
|
+
}
|
|
3295
|
+
const preferredTimePeriod = Object.entries(energyDistribution).sort(
|
|
3296
|
+
(a, b) => b[1] - a[1]
|
|
3297
|
+
)[0]?.[0] ?? "afternoon";
|
|
3298
|
+
const avgSessionMinutes = avg(sessions.map((s) => s.durationMinutes));
|
|
3299
|
+
const avgTurnsPerSession = avg(sessions.map((s) => s.turnCount));
|
|
3300
|
+
const engagementTrend = computeLinearTrend(sessions.map((s) => s.turnCount));
|
|
3301
|
+
const frustrationCorrelations = n >= MIN_SESSIONS_FOR_CORRELATIONS ? {
|
|
3302
|
+
toolErrors: pearsonR(
|
|
3303
|
+
sessions.map((s) => s.avgFrustration),
|
|
3304
|
+
sessions.map((s) => s.toolErrors)
|
|
3305
|
+
),
|
|
3306
|
+
longSessions: pearsonR(
|
|
3307
|
+
sessions.map((s) => s.avgFrustration),
|
|
3308
|
+
sessions.map((s) => s.durationMinutes)
|
|
3309
|
+
),
|
|
3310
|
+
lateNight: pearsonR(
|
|
3311
|
+
sessions.map((s) => s.avgFrustration),
|
|
3312
|
+
sessions.map((s) => s.timePeriod === "late-night" || s.timePeriod === "night" ? 1 : 0)
|
|
3313
|
+
)
|
|
3314
|
+
} : { toolErrors: 0, longSessions: 0, lateNight: 0 };
|
|
3315
|
+
const nudgeStats = {};
|
|
3316
|
+
for (const s of sessions) {
|
|
3317
|
+
const ratingVal = ratingToNumber(s.rating);
|
|
3318
|
+
for (const nudge of s.wellbeingNudges) {
|
|
3319
|
+
if (!nudgeStats[nudge]) nudgeStats[nudge] = { fired: 0, sessionRatingAfter: 0 };
|
|
3320
|
+
nudgeStats[nudge].fired++;
|
|
3321
|
+
nudgeStats[nudge].sessionRatingAfter += ratingVal;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
for (const key of Object.keys(nudgeStats)) {
|
|
3325
|
+
if (nudgeStats[key].fired > 0) {
|
|
3326
|
+
nudgeStats[key].sessionRatingAfter /= nudgeStats[key].fired;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
return {
|
|
3330
|
+
trustScore,
|
|
3331
|
+
trustTrajectory,
|
|
3332
|
+
totalSessions,
|
|
3333
|
+
preferredTimePeriod,
|
|
3334
|
+
energyDistribution,
|
|
3335
|
+
avgSessionMinutes,
|
|
3336
|
+
baselineFrustration,
|
|
3337
|
+
baselineExcitement,
|
|
3338
|
+
sentimentTrend,
|
|
3339
|
+
frustrationCorrelations,
|
|
3340
|
+
avgTurnsPerSession,
|
|
3341
|
+
engagementTrend,
|
|
3342
|
+
nudgeStats
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
function feedForward(model) {
|
|
3346
|
+
if (model.profile.totalSessions < MIN_SESSIONS_FOR_FEED_FORWARD) return null;
|
|
3347
|
+
const p4 = model.profile;
|
|
3348
|
+
const overrides = {
|
|
3349
|
+
compactGreeting: false,
|
|
3350
|
+
frustrationNudgeThreshold: 0.6,
|
|
3351
|
+
defaultToPersonalMode: false
|
|
3352
|
+
};
|
|
3353
|
+
const nightSessions = (p4.energyDistribution["late-night"] || 0) + (p4.energyDistribution["night"] || 0);
|
|
3354
|
+
const totalInWindow = model.sessions.length;
|
|
3355
|
+
if (totalInWindow > 0 && nightSessions / totalInWindow >= 0.7 && p4.baselineFrustration < 0.3) {
|
|
3356
|
+
overrides.energyOverride = "steady";
|
|
3357
|
+
}
|
|
3358
|
+
if (p4.trustScore > 0.8) {
|
|
3359
|
+
overrides.compactGreeting = true;
|
|
3360
|
+
}
|
|
3361
|
+
if (p4.frustrationCorrelations.toolErrors > 0.4) {
|
|
3362
|
+
overrides.frustrationNudgeThreshold = 0.4;
|
|
3363
|
+
}
|
|
3364
|
+
if (p4.sentimentTrend === "worsening") {
|
|
3365
|
+
overrides.defaultToPersonalMode = true;
|
|
3366
|
+
}
|
|
3367
|
+
return overrides;
|
|
3368
|
+
}
|
|
3369
|
+
function clamp(val, min, max) {
|
|
3370
|
+
return Math.max(min, Math.min(max, val));
|
|
3371
|
+
}
|
|
3372
|
+
function avg(values) {
|
|
3373
|
+
if (values.length === 0) return 0;
|
|
3374
|
+
return values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
3375
|
+
}
|
|
3376
|
+
function ratingSignal(session) {
|
|
3377
|
+
if (session.rating === "great") return 1;
|
|
3378
|
+
if (session.rating === "good") return 0.75;
|
|
3379
|
+
if (session.rating === "okay") return 0.5;
|
|
3380
|
+
if (session.rating === "frustrating") return 0.25;
|
|
3381
|
+
let implicit = 1;
|
|
3382
|
+
implicit -= session.avgFrustration * 0.4;
|
|
3383
|
+
implicit -= session.toolErrors > 3 ? 0.2 : 0;
|
|
3384
|
+
implicit -= session.blockers > 2 ? 0.2 : 0;
|
|
3385
|
+
implicit += session.milestones > 0 ? 0.1 : 0;
|
|
3386
|
+
return clamp(implicit, 0, 1);
|
|
3387
|
+
}
|
|
3388
|
+
function ratingToNumber(rating) {
|
|
3389
|
+
if (rating === "great") return 1;
|
|
3390
|
+
if (rating === "good") return 0.75;
|
|
3391
|
+
if (rating === "okay") return 0.5;
|
|
3392
|
+
if (rating === "frustrating") return 0.25;
|
|
3393
|
+
return 0.5;
|
|
3394
|
+
}
|
|
3395
|
+
function pearsonR(x, y) {
|
|
3396
|
+
const n = x.length;
|
|
3397
|
+
if (n < 3) return 0;
|
|
3398
|
+
const mx = avg(x);
|
|
3399
|
+
const my = avg(y);
|
|
3400
|
+
let num = 0;
|
|
3401
|
+
let dx2 = 0;
|
|
3402
|
+
let dy2 = 0;
|
|
3403
|
+
for (let i = 0; i < n; i++) {
|
|
3404
|
+
const dx = x[i] - mx;
|
|
3405
|
+
const dy = y[i] - my;
|
|
3406
|
+
num += dx * dy;
|
|
3407
|
+
dx2 += dx * dx;
|
|
3408
|
+
dy2 += dy * dy;
|
|
3409
|
+
}
|
|
3410
|
+
const denom = Math.sqrt(dx2 * dy2);
|
|
3411
|
+
if (denom === 0) return 0;
|
|
3412
|
+
return num / denom;
|
|
3413
|
+
}
|
|
3414
|
+
function computeTrustTrajectory(sessions) {
|
|
3415
|
+
if (sessions.length < 10) return "stable";
|
|
3416
|
+
const recent5 = sessions.slice(-5).map(ratingSignal);
|
|
3417
|
+
const prev5 = sessions.slice(-10, -5).map(ratingSignal);
|
|
3418
|
+
const recentAvg = avg(recent5);
|
|
3419
|
+
const prevAvg = avg(prev5);
|
|
3420
|
+
const delta = recentAvg - prevAvg;
|
|
3421
|
+
if (delta > 0.1) return "ascending";
|
|
3422
|
+
if (delta < -0.1) return "declining";
|
|
3423
|
+
return "stable";
|
|
3424
|
+
}
|
|
3425
|
+
function computeSentimentTrend(sessions) {
|
|
3426
|
+
if (sessions.length < 5) return "stable";
|
|
3427
|
+
const frustrations = sessions.slice(-10).map((s) => s.avgFrustration);
|
|
3428
|
+
const slope = linearSlope(frustrations);
|
|
3429
|
+
if (slope > 0.02) return "worsening";
|
|
3430
|
+
if (slope < -0.02) return "improving";
|
|
3431
|
+
return "stable";
|
|
3432
|
+
}
|
|
3433
|
+
function computeLinearTrend(values) {
|
|
3434
|
+
if (values.length < 5) return "stable";
|
|
3435
|
+
const recent = values.slice(-10);
|
|
3436
|
+
const slope = linearSlope(recent);
|
|
3437
|
+
const mean = avg(recent);
|
|
3438
|
+
const relativeSlope = mean > 0 ? slope / mean : slope;
|
|
3439
|
+
if (relativeSlope > 0.03) return "increasing";
|
|
3440
|
+
if (relativeSlope < -0.03) return "decreasing";
|
|
3441
|
+
return "stable";
|
|
3442
|
+
}
|
|
3443
|
+
function linearSlope(values) {
|
|
3444
|
+
const n = values.length;
|
|
3445
|
+
if (n < 2) return 0;
|
|
3446
|
+
let sumX = 0;
|
|
3447
|
+
let sumY = 0;
|
|
3448
|
+
let sumXY = 0;
|
|
3449
|
+
let sumX2 = 0;
|
|
3450
|
+
for (let i = 0; i < n; i++) {
|
|
3451
|
+
sumX += i;
|
|
3452
|
+
sumY += values[i];
|
|
3453
|
+
sumXY += i * values[i];
|
|
3454
|
+
sumX2 += i * i;
|
|
3455
|
+
}
|
|
3456
|
+
const denom = n * sumX2 - sumX * sumX;
|
|
3457
|
+
if (denom === 0) return 0;
|
|
3458
|
+
return (n * sumXY - sumX * sumY) / denom;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
2914
3461
|
// src/hooks.ts
|
|
2915
3462
|
function getTimeContext() {
|
|
2916
3463
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -3058,6 +3605,29 @@ ${contextInjection}`;
|
|
|
3058
3605
|
sessionMinutes: 0,
|
|
3059
3606
|
turnCount: 0
|
|
3060
3607
|
});
|
|
3608
|
+
try {
|
|
3609
|
+
const model = await loadUserModel();
|
|
3610
|
+
if (model) {
|
|
3611
|
+
const overrides = feedForward(model);
|
|
3612
|
+
if (overrides) {
|
|
3613
|
+
log.debug("hooks", `Feed-forward active (trust=${model.profile.trustScore.toFixed(2)}, sessions=${model.profile.totalSessions})`);
|
|
3614
|
+
if (overrides.energyOverride && (period === "late-night" || period === "night")) {
|
|
3615
|
+
state.energy = overrides.energyOverride;
|
|
3616
|
+
}
|
|
3617
|
+
if (overrides.defaultToPersonalMode && state.activeMode === "Default") {
|
|
3618
|
+
state.activeMode = "Personal";
|
|
3619
|
+
}
|
|
3620
|
+
if (overrides.compactGreeting) {
|
|
3621
|
+
greeting += "\n<user-model-context>High trust user (score: " + model.profile.trustScore.toFixed(2) + ", " + model.profile.totalSessions + " sessions). Keep greeting compact \u2014 they know you well.</user-model-context>";
|
|
3622
|
+
}
|
|
3623
|
+
if (model.profile.sentimentTrend === "worsening") {
|
|
3624
|
+
greeting += "\n<user-model-context>Sentiment trend is worsening across recent sessions. Be more attentive and patient.</user-model-context>";
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
} catch (err) {
|
|
3629
|
+
log.debug("hooks", "user model feed-forward failed", err);
|
|
3630
|
+
}
|
|
3061
3631
|
syncPersonalityToCore(state, ctx.mcpManager).catch(() => {
|
|
3062
3632
|
});
|
|
3063
3633
|
const nudge = formatWellbeingNudge(state);
|
|
@@ -3179,10 +3749,10 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
|
|
|
3179
3749
|
}
|
|
3180
3750
|
console.log(pc2.dim(` Saved ${textMessages.length} messages (session: ${sessionId})`));
|
|
3181
3751
|
}
|
|
3182
|
-
const projectContextPath =
|
|
3183
|
-
if (
|
|
3752
|
+
const projectContextPath = path14.join(process.cwd(), ".acore", "context.md");
|
|
3753
|
+
if (fs14.existsSync(projectContextPath) && messages.length > 2) {
|
|
3184
3754
|
try {
|
|
3185
|
-
let contextContent =
|
|
3755
|
+
let contextContent = fs14.readFileSync(projectContextPath, "utf-8");
|
|
3186
3756
|
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3187
3757
|
let lastUserMsg = "";
|
|
3188
3758
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -3200,28 +3770,28 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
|
|
|
3200
3770
|
- Recent decisions: [see memory]
|
|
3201
3771
|
- Temp notes: [cleared]`;
|
|
3202
3772
|
contextContent = contextContent.replace(sessionPattern, newSession);
|
|
3203
|
-
|
|
3773
|
+
fs14.writeFileSync(projectContextPath, contextContent, "utf-8");
|
|
3204
3774
|
log.debug("hooks", `Updated project context: ${projectContextPath}`);
|
|
3205
3775
|
}
|
|
3206
3776
|
} catch (err) {
|
|
3207
3777
|
log.debug("hooks", "project context update failed", err);
|
|
3208
3778
|
}
|
|
3209
3779
|
}
|
|
3780
|
+
const sessionMinutes = Math.round((Date.now() - sessionStartTime) / 6e4);
|
|
3781
|
+
const hour = (/* @__PURE__ */ new Date()).getHours();
|
|
3782
|
+
let period;
|
|
3783
|
+
if (hour < 6) period = "late-night";
|
|
3784
|
+
else if (hour < 12) period = "morning";
|
|
3785
|
+
else if (hour < 17) period = "afternoon";
|
|
3786
|
+
else if (hour < 21) period = "evening";
|
|
3787
|
+
else period = "night";
|
|
3788
|
+
const turnCount = messages.filter((m) => m.role === "user").length;
|
|
3789
|
+
const finalState = computePersonality({
|
|
3790
|
+
timePeriod: period,
|
|
3791
|
+
sessionMinutes,
|
|
3792
|
+
turnCount
|
|
3793
|
+
});
|
|
3210
3794
|
if (ctx.config.personalityAdapt !== false) {
|
|
3211
|
-
const sessionMinutes = Math.round((Date.now() - sessionStartTime) / 6e4);
|
|
3212
|
-
const hour = (/* @__PURE__ */ new Date()).getHours();
|
|
3213
|
-
let period;
|
|
3214
|
-
if (hour < 6) period = "late-night";
|
|
3215
|
-
else if (hour < 12) period = "morning";
|
|
3216
|
-
else if (hour < 17) period = "afternoon";
|
|
3217
|
-
else if (hour < 21) period = "evening";
|
|
3218
|
-
else period = "night";
|
|
3219
|
-
const turnCount = messages.filter((m) => m.role === "user").length;
|
|
3220
|
-
const finalState = computePersonality({
|
|
3221
|
-
timePeriod: period,
|
|
3222
|
-
sessionMinutes,
|
|
3223
|
-
turnCount
|
|
3224
|
-
});
|
|
3225
3795
|
try {
|
|
3226
3796
|
isHookCall = true;
|
|
3227
3797
|
await syncPersonalityToCore(finalState, ctx.mcpManager);
|
|
@@ -3229,6 +3799,7 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
|
|
|
3229
3799
|
isHookCall = false;
|
|
3230
3800
|
}
|
|
3231
3801
|
}
|
|
3802
|
+
let sessionRating;
|
|
3232
3803
|
if (ctx.config.evalPrompt) {
|
|
3233
3804
|
const rating = await p2.select({
|
|
3234
3805
|
message: "Quick rating for this session?",
|
|
@@ -3241,10 +3812,11 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
|
|
|
3241
3812
|
initialValue: "skip"
|
|
3242
3813
|
});
|
|
3243
3814
|
if (!p2.isCancel(rating) && rating !== "skip") {
|
|
3815
|
+
sessionRating = rating;
|
|
3244
3816
|
try {
|
|
3245
3817
|
isHookCall = true;
|
|
3246
3818
|
await ctx.mcpManager.callTool("eval_log", {
|
|
3247
|
-
rating,
|
|
3819
|
+
rating: sessionRating,
|
|
3248
3820
|
highlights: "Quick session rating",
|
|
3249
3821
|
improvements: ""
|
|
3250
3822
|
});
|
|
@@ -3253,6 +3825,51 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
|
|
|
3253
3825
|
}
|
|
3254
3826
|
}
|
|
3255
3827
|
}
|
|
3828
|
+
if (turnCount >= 2 && sessionMinutes >= 1) {
|
|
3829
|
+
try {
|
|
3830
|
+
const snapshot = {
|
|
3831
|
+
sessionId,
|
|
3832
|
+
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
3833
|
+
durationMinutes: sessionMinutes,
|
|
3834
|
+
turnCount,
|
|
3835
|
+
dominantSentiment: finalState.sentiment.dominant,
|
|
3836
|
+
avgFrustration: finalState.sentiment.frustration,
|
|
3837
|
+
avgExcitement: finalState.sentiment.excitement,
|
|
3838
|
+
avgConfusion: finalState.sentiment.confusion,
|
|
3839
|
+
avgFatigue: finalState.sentiment.fatigue,
|
|
3840
|
+
toolCalls: observationSession?.stats.toolCalls ?? 0,
|
|
3841
|
+
toolErrors: observationSession?.stats.toolErrors ?? 0,
|
|
3842
|
+
blockers: observationSession?.stats.blockers ?? 0,
|
|
3843
|
+
milestones: observationSession?.stats.milestones ?? 0,
|
|
3844
|
+
topicShifts: observationSession?.stats.topicShifts ?? 0,
|
|
3845
|
+
peakEnergy: finalState.energy,
|
|
3846
|
+
primaryMode: finalState.activeMode,
|
|
3847
|
+
timePeriod: period,
|
|
3848
|
+
rating: sessionRating,
|
|
3849
|
+
hadPostmortem: false,
|
|
3850
|
+
// updated below if postmortem is generated
|
|
3851
|
+
wellbeingNudges: finalState.wellbeingNudge ? [finalState.wellbeingNudge] : []
|
|
3852
|
+
};
|
|
3853
|
+
const model = await loadUserModel() ?? createEmptyModel();
|
|
3854
|
+
const updated = aggregateSession(model, snapshot);
|
|
3855
|
+
await saveUserModel(updated);
|
|
3856
|
+
log.debug("hooks", `User model updated (session ${updated.profile.totalSessions})`);
|
|
3857
|
+
if (ctx.config.personalityAdapt !== false) {
|
|
3858
|
+
try {
|
|
3859
|
+
isHookCall = true;
|
|
3860
|
+
await syncPersonalityToCore(finalState, ctx.mcpManager, {
|
|
3861
|
+
trustScore: updated.profile.trustScore,
|
|
3862
|
+
totalSessions: updated.profile.totalSessions,
|
|
3863
|
+
sentimentTrend: updated.profile.sentimentTrend
|
|
3864
|
+
});
|
|
3865
|
+
} finally {
|
|
3866
|
+
isHookCall = false;
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
} catch (err) {
|
|
3870
|
+
log.debug("hooks", "user model aggregation failed", err);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3256
3873
|
if (ctx.config.autoPostmortem !== false && observationSession && shouldAutoPostmortem(observationSession, messages)) {
|
|
3257
3874
|
try {
|
|
3258
3875
|
const client = ctx.llmClient;
|
|
@@ -3278,6 +3895,75 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
|
|
|
3278
3895
|
} catch {
|
|
3279
3896
|
}
|
|
3280
3897
|
}
|
|
3898
|
+
if (report.crystallizationCandidates && report.crystallizationCandidates.length > 0) {
|
|
3899
|
+
const skillsMdPath = path14.join(os13.homedir(), ".askill", "skills.md");
|
|
3900
|
+
const logPath = path14.join(
|
|
3901
|
+
os13.homedir(),
|
|
3902
|
+
".aman-agent",
|
|
3903
|
+
"crystallization-log.json"
|
|
3904
|
+
);
|
|
3905
|
+
const rejectionsPath = path14.join(
|
|
3906
|
+
os13.homedir(),
|
|
3907
|
+
".aman-agent",
|
|
3908
|
+
"crystallization-rejections.json"
|
|
3909
|
+
);
|
|
3910
|
+
const postmortemFilename = `${report.date}-${report.sessionId.slice(0, 4)}.md`;
|
|
3911
|
+
console.log(
|
|
3912
|
+
pc2.dim(`
|
|
3913
|
+
Crystallization candidates: ${report.crystallizationCandidates.length}`)
|
|
3914
|
+
);
|
|
3915
|
+
let skipAll = false;
|
|
3916
|
+
for (const rawCandidate of report.crystallizationCandidates) {
|
|
3917
|
+
if (skipAll) break;
|
|
3918
|
+
const candidate = validateCandidate(rawCandidate);
|
|
3919
|
+
if (!candidate) {
|
|
3920
|
+
log.debug("hooks", "candidate failed validation");
|
|
3921
|
+
continue;
|
|
3922
|
+
}
|
|
3923
|
+
const choice = await p2.select({
|
|
3924
|
+
message: `Crystallize "${candidate.name}" as a reusable skill?`,
|
|
3925
|
+
options: [
|
|
3926
|
+
{ value: "accept", label: "Yes \u2014 write to ~/.askill/skills.md" },
|
|
3927
|
+
{ value: "reject", label: "No \u2014 skip this one" },
|
|
3928
|
+
{ value: "skip-all", label: "Skip all crystallization for this session" }
|
|
3929
|
+
],
|
|
3930
|
+
initialValue: "reject"
|
|
3931
|
+
});
|
|
3932
|
+
if (p2.isCancel(choice) || choice === "skip-all") {
|
|
3933
|
+
skipAll = true;
|
|
3934
|
+
break;
|
|
3935
|
+
}
|
|
3936
|
+
if (choice === "accept") {
|
|
3937
|
+
const result = await writeSkillToFile(
|
|
3938
|
+
candidate,
|
|
3939
|
+
skillsMdPath,
|
|
3940
|
+
postmortemFilename
|
|
3941
|
+
);
|
|
3942
|
+
if (result.written) {
|
|
3943
|
+
console.log(
|
|
3944
|
+
pc2.green(` \u2713 Crystallized: ${candidate.name} \u2192 ${result.filePath}`)
|
|
3945
|
+
);
|
|
3946
|
+
console.log(pc2.dim(` Triggers: ${candidate.triggers.join(", ")}`));
|
|
3947
|
+
console.log(pc2.dim(` Will auto-activate next session.`));
|
|
3948
|
+
await appendCrystallizationLog(
|
|
3949
|
+
{
|
|
3950
|
+
name: candidate.name,
|
|
3951
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3952
|
+
fromPostmortem: postmortemFilename,
|
|
3953
|
+
confidence: candidate.confidence,
|
|
3954
|
+
triggers: candidate.triggers
|
|
3955
|
+
},
|
|
3956
|
+
logPath
|
|
3957
|
+
);
|
|
3958
|
+
} else {
|
|
3959
|
+
console.log(pc2.yellow(` \u2298 Could not crystallize: ${result.reason}`));
|
|
3960
|
+
}
|
|
3961
|
+
} else {
|
|
3962
|
+
console.log(pc2.dim(` Skipped: ${candidate.name}`));
|
|
3963
|
+
await appendRejection(candidate, postmortemFilename, rejectionsPath);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3281
3967
|
}
|
|
3282
3968
|
}
|
|
3283
3969
|
} catch (err) {
|
|
@@ -3437,43 +4123,43 @@ async function delegatePipeline(steps, initialInput, client, mcpManager, options
|
|
|
3437
4123
|
}
|
|
3438
4124
|
|
|
3439
4125
|
// src/teams.ts
|
|
3440
|
-
import
|
|
3441
|
-
import
|
|
3442
|
-
import
|
|
4126
|
+
import fs15 from "fs";
|
|
4127
|
+
import path15 from "path";
|
|
4128
|
+
import os14 from "os";
|
|
3443
4129
|
import pc4 from "picocolors";
|
|
3444
4130
|
function getTeamsDir() {
|
|
3445
|
-
return
|
|
4131
|
+
return path15.join(os14.homedir(), ".acore", "teams");
|
|
3446
4132
|
}
|
|
3447
4133
|
function ensureTeamsDir() {
|
|
3448
4134
|
const dir = getTeamsDir();
|
|
3449
|
-
if (!
|
|
4135
|
+
if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
|
|
3450
4136
|
return dir;
|
|
3451
4137
|
}
|
|
3452
4138
|
function teamPath(name) {
|
|
3453
4139
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
3454
|
-
return
|
|
4140
|
+
return path15.join(ensureTeamsDir(), `${slug}.json`);
|
|
3455
4141
|
}
|
|
3456
4142
|
function createTeam(team) {
|
|
3457
4143
|
const fp = teamPath(team.name);
|
|
3458
|
-
|
|
4144
|
+
fs15.writeFileSync(fp, JSON.stringify(team, null, 2), "utf-8");
|
|
3459
4145
|
}
|
|
3460
4146
|
function loadTeam(name) {
|
|
3461
4147
|
const fp = teamPath(name);
|
|
3462
|
-
if (!
|
|
4148
|
+
if (!fs15.existsSync(fp)) return null;
|
|
3463
4149
|
try {
|
|
3464
|
-
return JSON.parse(
|
|
4150
|
+
return JSON.parse(fs15.readFileSync(fp, "utf-8"));
|
|
3465
4151
|
} catch {
|
|
3466
4152
|
return null;
|
|
3467
4153
|
}
|
|
3468
4154
|
}
|
|
3469
4155
|
function listTeams() {
|
|
3470
4156
|
const dir = getTeamsDir();
|
|
3471
|
-
if (!
|
|
4157
|
+
if (!fs15.existsSync(dir)) return [];
|
|
3472
4158
|
const teams = [];
|
|
3473
|
-
for (const file of
|
|
4159
|
+
for (const file of fs15.readdirSync(dir)) {
|
|
3474
4160
|
if (!file.endsWith(".json")) continue;
|
|
3475
4161
|
try {
|
|
3476
|
-
const content =
|
|
4162
|
+
const content = fs15.readFileSync(path15.join(dir, file), "utf-8");
|
|
3477
4163
|
teams.push(JSON.parse(content));
|
|
3478
4164
|
} catch {
|
|
3479
4165
|
}
|
|
@@ -3482,8 +4168,8 @@ function listTeams() {
|
|
|
3482
4168
|
}
|
|
3483
4169
|
function deleteTeam(name) {
|
|
3484
4170
|
const fp = teamPath(name);
|
|
3485
|
-
if (!
|
|
3486
|
-
|
|
4171
|
+
if (!fs15.existsSync(fp)) return false;
|
|
4172
|
+
fs15.unlinkSync(fp);
|
|
3487
4173
|
return true;
|
|
3488
4174
|
}
|
|
3489
4175
|
async function runTeam(team, task, client, mcpManager, tools) {
|
|
@@ -3709,23 +4395,23 @@ var BUILT_IN_TEAMS = [
|
|
|
3709
4395
|
];
|
|
3710
4396
|
|
|
3711
4397
|
// src/plans.ts
|
|
3712
|
-
import
|
|
3713
|
-
import
|
|
3714
|
-
import
|
|
4398
|
+
import fs16 from "fs";
|
|
4399
|
+
import path16 from "path";
|
|
4400
|
+
import os15 from "os";
|
|
3715
4401
|
function getPlansDir() {
|
|
3716
|
-
const localDir =
|
|
3717
|
-
const localAcore =
|
|
3718
|
-
if (
|
|
3719
|
-
return
|
|
4402
|
+
const localDir = path16.join(process.cwd(), ".acore", "plans");
|
|
4403
|
+
const localAcore = path16.join(process.cwd(), ".acore");
|
|
4404
|
+
if (fs16.existsSync(localAcore)) return localDir;
|
|
4405
|
+
return path16.join(os15.homedir(), ".acore", "plans");
|
|
3720
4406
|
}
|
|
3721
4407
|
function ensurePlansDir() {
|
|
3722
4408
|
const dir = getPlansDir();
|
|
3723
|
-
if (!
|
|
4409
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
3724
4410
|
return dir;
|
|
3725
4411
|
}
|
|
3726
4412
|
function planPath(name) {
|
|
3727
4413
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
3728
|
-
return
|
|
4414
|
+
return path16.join(ensurePlansDir(), `${slug}.md`);
|
|
3729
4415
|
}
|
|
3730
4416
|
function serializePlan(plan) {
|
|
3731
4417
|
const lines = [];
|
|
@@ -3751,7 +4437,7 @@ function parsePlan(content, filePath) {
|
|
|
3751
4437
|
const createdMatch = content.match(/\*\*Created:\*\*\s*(.+)/);
|
|
3752
4438
|
const updatedMatch = content.match(/\*\*Updated:\*\*\s*(.+)/);
|
|
3753
4439
|
const activeMatch = content.match(/\*\*Active:\*\*\s*(.+)/);
|
|
3754
|
-
const name = nameMatch?.[1]?.trim() ||
|
|
4440
|
+
const name = nameMatch?.[1]?.trim() || path16.basename(filePath, ".md");
|
|
3755
4441
|
const goal = goalMatch?.[1]?.trim() || "";
|
|
3756
4442
|
const createdAt = createdMatch?.[1]?.trim() || "";
|
|
3757
4443
|
const updatedAt = updatedMatch?.[1]?.trim() || "";
|
|
@@ -3793,22 +4479,22 @@ function createPlan(name, goal, steps) {
|
|
|
3793
4479
|
}
|
|
3794
4480
|
function savePlan(plan) {
|
|
3795
4481
|
const fp = planPath(plan.name);
|
|
3796
|
-
|
|
4482
|
+
fs16.writeFileSync(fp, serializePlan(plan), "utf-8");
|
|
3797
4483
|
}
|
|
3798
4484
|
function loadPlan(name) {
|
|
3799
4485
|
const fp = planPath(name);
|
|
3800
|
-
if (!
|
|
3801
|
-
const content =
|
|
4486
|
+
if (!fs16.existsSync(fp)) return null;
|
|
4487
|
+
const content = fs16.readFileSync(fp, "utf-8");
|
|
3802
4488
|
return parsePlan(content, fp);
|
|
3803
4489
|
}
|
|
3804
4490
|
function listPlans() {
|
|
3805
4491
|
const dir = getPlansDir();
|
|
3806
|
-
if (!
|
|
4492
|
+
if (!fs16.existsSync(dir)) return [];
|
|
3807
4493
|
const plans = [];
|
|
3808
|
-
for (const file of
|
|
4494
|
+
for (const file of fs16.readdirSync(dir)) {
|
|
3809
4495
|
if (!file.endsWith(".md")) continue;
|
|
3810
|
-
const fp =
|
|
3811
|
-
const content =
|
|
4496
|
+
const fp = path16.join(dir, file);
|
|
4497
|
+
const content = fs16.readFileSync(fp, "utf-8");
|
|
3812
4498
|
const plan = parsePlan(content, fp);
|
|
3813
4499
|
if (plan) plans.push(plan);
|
|
3814
4500
|
}
|
|
@@ -3917,10 +4603,10 @@ import {
|
|
|
3917
4603
|
} from "@aman_asmuei/arules-core";
|
|
3918
4604
|
var AGENT_SCOPE = process.env.AMAN_AGENT_SCOPE ?? "dev:agent";
|
|
3919
4605
|
function readEcosystemFile(filePath, label) {
|
|
3920
|
-
if (!
|
|
4606
|
+
if (!fs17.existsSync(filePath)) {
|
|
3921
4607
|
return pc5.dim(`No ${label} file found at ${filePath}`);
|
|
3922
4608
|
}
|
|
3923
|
-
return
|
|
4609
|
+
return fs17.readFileSync(filePath, "utf-8").trim();
|
|
3924
4610
|
}
|
|
3925
4611
|
function parseCommand(input) {
|
|
3926
4612
|
const trimmed = input.trim();
|
|
@@ -3998,24 +4684,76 @@ async function handleIdentityCommand(action, args, _ctx) {
|
|
|
3998
4684
|
}
|
|
3999
4685
|
}
|
|
4000
4686
|
if (action === "dynamics") {
|
|
4687
|
+
if (args.includes("--json")) {
|
|
4688
|
+
const model2 = await loadUserModel();
|
|
4689
|
+
if (!model2) return { handled: true, output: pc5.dim("No user model yet. Complete a few sessions first.") };
|
|
4690
|
+
return { handled: true, output: JSON.stringify(model2, null, 2) };
|
|
4691
|
+
}
|
|
4692
|
+
if (args.includes("--reset")) {
|
|
4693
|
+
const modelPath = defaultModelPath();
|
|
4694
|
+
if (fs17.existsSync(modelPath)) {
|
|
4695
|
+
fs17.unlinkSync(modelPath);
|
|
4696
|
+
return { handled: true, output: pc5.green("User model reset. Starting fresh.") };
|
|
4697
|
+
}
|
|
4698
|
+
return { handled: true, output: pc5.dim("No user model to reset.") };
|
|
4699
|
+
}
|
|
4001
4700
|
const updates = {};
|
|
4002
4701
|
for (const arg of args) {
|
|
4003
4702
|
const eq = arg.indexOf("=");
|
|
4004
4703
|
if (eq > 0) updates[arg.slice(0, eq)] = arg.slice(eq + 1);
|
|
4005
4704
|
}
|
|
4006
|
-
if (
|
|
4007
|
-
|
|
4705
|
+
if (Object.keys(updates).length > 0) {
|
|
4706
|
+
try {
|
|
4707
|
+
await acoreUpdateDynamics({
|
|
4708
|
+
energy: updates.energy,
|
|
4709
|
+
activeMode: updates.mode,
|
|
4710
|
+
currentRead: updates.read
|
|
4711
|
+
}, AGENT_SCOPE);
|
|
4712
|
+
return { handled: true, output: `Dynamics updated: ${Object.entries(updates).map(([k, v]) => `${k}=${v}`).join(", ")}` };
|
|
4713
|
+
} catch (err) {
|
|
4714
|
+
return { handled: true, output: pc5.red(`Dynamics error: ${err instanceof Error ? err.message : String(err)}`) };
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
const model = await loadUserModel();
|
|
4718
|
+
if (!model) {
|
|
4719
|
+
return { handled: true, output: pc5.dim("No user model yet. Complete a few sessions to start building your profile.") };
|
|
4008
4720
|
}
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4721
|
+
const p4 = model.profile;
|
|
4722
|
+
const trustBar = "\u2588".repeat(Math.round(p4.trustScore * 10)) + "\u2591".repeat(10 - Math.round(p4.trustScore * 10));
|
|
4723
|
+
const frustBar = "\u2588".repeat(Math.round(p4.baselineFrustration * 10)) + "\u2591".repeat(10 - Math.round(p4.baselineFrustration * 10));
|
|
4724
|
+
const lines = [
|
|
4725
|
+
pc5.bold(" Dynamic User Model"),
|
|
4726
|
+
"",
|
|
4727
|
+
` ${pc5.cyan("Trust")} ${trustBar} ${(p4.trustScore * 100).toFixed(0)}% ${p4.trustTrajectory === "ascending" ? pc5.green("\u2191") : p4.trustTrajectory === "declining" ? pc5.red("\u2193") : "\u2192"}`,
|
|
4728
|
+
` ${pc5.cyan("Sessions")} ${p4.totalSessions} total (${model.sessions.length} in window)`,
|
|
4729
|
+
` ${pc5.cyan("Sentiment")} ${frustBar} frustration baseline ${p4.sentimentTrend === "improving" ? pc5.green("improving") : p4.sentimentTrend === "worsening" ? pc5.red("worsening") : "stable"}`,
|
|
4730
|
+
"",
|
|
4731
|
+
` ${pc5.cyan("Preferred")} ${p4.preferredTimePeriod} (${Object.entries(p4.energyDistribution).map(([k, v]) => `${k}: ${v}`).join(", ")})`,
|
|
4732
|
+
` ${pc5.cyan("Avg session")} ${p4.avgSessionMinutes.toFixed(0)} min, ${p4.avgTurnsPerSession.toFixed(0)} turns ${p4.engagementTrend === "increasing" ? pc5.green("\u2191") : p4.engagementTrend === "decreasing" ? pc5.red("\u2193") : "\u2192"}`
|
|
4733
|
+
];
|
|
4734
|
+
if (p4.totalSessions >= 10) {
|
|
4735
|
+
const corrs = [];
|
|
4736
|
+
if (Math.abs(p4.frustrationCorrelations.toolErrors) > 0.3) {
|
|
4737
|
+
corrs.push(`tool errors (${p4.frustrationCorrelations.toolErrors.toFixed(2)})`);
|
|
4738
|
+
}
|
|
4739
|
+
if (Math.abs(p4.frustrationCorrelations.longSessions) > 0.3) {
|
|
4740
|
+
corrs.push(`long sessions (${p4.frustrationCorrelations.longSessions.toFixed(2)})`);
|
|
4741
|
+
}
|
|
4742
|
+
if (Math.abs(p4.frustrationCorrelations.lateNight) > 0.3) {
|
|
4743
|
+
corrs.push(`late night (${p4.frustrationCorrelations.lateNight.toFixed(2)})`);
|
|
4744
|
+
}
|
|
4745
|
+
if (corrs.length > 0) {
|
|
4746
|
+
lines.push(` ${pc5.cyan("Frustration")} correlates with: ${corrs.join(", ")}`);
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
const nudgeKeys = Object.keys(p4.nudgeStats);
|
|
4750
|
+
if (nudgeKeys.length > 0) {
|
|
4751
|
+
lines.push("");
|
|
4752
|
+
lines.push(` ${pc5.cyan("Nudges")} ${nudgeKeys.map((k) => `${k}: ${p4.nudgeStats[k].fired}\xD7`).join(", ")}`);
|
|
4018
4753
|
}
|
|
4754
|
+
lines.push("");
|
|
4755
|
+
lines.push(pc5.dim(` Use --json for raw data, --reset to start fresh`));
|
|
4756
|
+
return { handled: true, output: lines.join("\n") };
|
|
4019
4757
|
}
|
|
4020
4758
|
if (action === "summary") {
|
|
4021
4759
|
try {
|
|
@@ -4039,7 +4777,10 @@ async function handleIdentityCommand(action, args, _ctx) {
|
|
|
4039
4777
|
pc5.bold("Identity commands:"),
|
|
4040
4778
|
` ${pc5.cyan("/identity")} View current identity`,
|
|
4041
4779
|
` ${pc5.cyan("/identity update")} <section> Update a section`,
|
|
4780
|
+
` ${pc5.cyan("/identity dynamics")} View user model (trust, sentiment, patterns)`,
|
|
4042
4781
|
` ${pc5.cyan("/identity dynamics")} key=val Update dynamic fields (energy, mode, read)`,
|
|
4782
|
+
` ${pc5.cyan("/identity dynamics")} --json Raw JSON user model`,
|
|
4783
|
+
` ${pc5.cyan("/identity dynamics")} --reset Reset user model`,
|
|
4043
4784
|
` ${pc5.cyan("/identity summary")} Show structured identity summary`
|
|
4044
4785
|
].join("\n")
|
|
4045
4786
|
};
|
|
@@ -4195,9 +4936,9 @@ ${result.violations.map((v) => ` - ${v}`).join("\n")}`)
|
|
|
4195
4936
|
};
|
|
4196
4937
|
}
|
|
4197
4938
|
async function handleWorkflowsCommand(action, args, ctx) {
|
|
4198
|
-
const home2 =
|
|
4939
|
+
const home2 = os16.homedir();
|
|
4199
4940
|
if (!action) {
|
|
4200
|
-
const content = readEcosystemFile(
|
|
4941
|
+
const content = readEcosystemFile(path17.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
|
|
4201
4942
|
return { handled: true, output: content };
|
|
4202
4943
|
}
|
|
4203
4944
|
if (action === "add") {
|
|
@@ -4219,7 +4960,7 @@ async function handleWorkflowsCommand(action, args, ctx) {
|
|
|
4219
4960
|
return { handled: true, output: pc5.yellow("Usage: /workflows get <name>") };
|
|
4220
4961
|
}
|
|
4221
4962
|
const name = args.join(" ").toLowerCase();
|
|
4222
|
-
const raw = readEcosystemFile(
|
|
4963
|
+
const raw = readEcosystemFile(path17.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
|
|
4223
4964
|
if (raw.startsWith("No ")) {
|
|
4224
4965
|
return { handled: true, output: raw };
|
|
4225
4966
|
}
|
|
@@ -4278,12 +5019,12 @@ async function handleToolsCommand(action, args, _ctx) {
|
|
|
4278
5019
|
return { handled: true, output: pc5.yellow("Usage: /tools search <query...>") };
|
|
4279
5020
|
}
|
|
4280
5021
|
const query = args.join(" ").toLowerCase();
|
|
4281
|
-
const home2 =
|
|
4282
|
-
const toolsFile =
|
|
4283
|
-
if (!
|
|
5022
|
+
const home2 = os16.homedir();
|
|
5023
|
+
const toolsFile = path17.join(home2, ".akit", "tools.md");
|
|
5024
|
+
if (!fs17.existsSync(toolsFile)) {
|
|
4284
5025
|
return { handled: true, output: pc5.dim(`No tools file found. Use 'npx @aman_asmuei/akit search ${args.join(" ")}' to search the registry.`) };
|
|
4285
5026
|
}
|
|
4286
|
-
const raw =
|
|
5027
|
+
const raw = fs17.readFileSync(toolsFile, "utf-8").trim();
|
|
4287
5028
|
const lines = raw.split("\n");
|
|
4288
5029
|
const matches = lines.filter((l) => l.toLowerCase().includes(query));
|
|
4289
5030
|
if (matches.length === 0) {
|
|
@@ -4294,9 +5035,9 @@ async function handleToolsCommand(action, args, _ctx) {
|
|
|
4294
5035
|
return handleAkitCommand(action, args);
|
|
4295
5036
|
}
|
|
4296
5037
|
async function handleSkillsCommand(action, args, ctx) {
|
|
4297
|
-
const home2 =
|
|
5038
|
+
const home2 = os16.homedir();
|
|
4298
5039
|
if (!action) {
|
|
4299
|
-
const content = readEcosystemFile(
|
|
5040
|
+
const content = readEcosystemFile(path17.join(home2, ".askill", "skills.md"), "skills (askill)");
|
|
4300
5041
|
return { handled: true, output: content };
|
|
4301
5042
|
}
|
|
4302
5043
|
if (action === "install") {
|
|
@@ -4318,8 +5059,8 @@ async function handleSkillsCommand(action, args, ctx) {
|
|
|
4318
5059
|
return { handled: true, output: pc5.yellow("Usage: /skills search <query...>") };
|
|
4319
5060
|
}
|
|
4320
5061
|
const query = args.join(" ").toLowerCase();
|
|
4321
|
-
const home3 =
|
|
4322
|
-
const raw = readEcosystemFile(
|
|
5062
|
+
const home3 = os16.homedir();
|
|
5063
|
+
const raw = readEcosystemFile(path17.join(home3, ".askill", "skills.md"), "skills (askill)");
|
|
4323
5064
|
if (raw.startsWith("No ")) {
|
|
4324
5065
|
return { handled: true, output: raw };
|
|
4325
5066
|
}
|
|
@@ -4330,21 +5071,111 @@ async function handleSkillsCommand(action, args, ctx) {
|
|
|
4330
5071
|
}
|
|
4331
5072
|
return { handled: true, output: [pc5.bold(`Skills matching "${query}":`), ...matches].join("\n") };
|
|
4332
5073
|
}
|
|
5074
|
+
if (action === "list") {
|
|
5075
|
+
const autoOnly = args.includes("--auto");
|
|
5076
|
+
if (autoOnly) {
|
|
5077
|
+
const logPath = path17.join(os16.homedir(), ".aman-agent", "crystallization-log.json");
|
|
5078
|
+
try {
|
|
5079
|
+
const content2 = fs17.readFileSync(logPath, "utf-8");
|
|
5080
|
+
const entries = JSON.parse(content2);
|
|
5081
|
+
if (entries.length === 0) {
|
|
5082
|
+
return { handled: true, output: pc5.dim("No crystallized skills yet.") };
|
|
5083
|
+
}
|
|
5084
|
+
const lines = [pc5.bold(`Crystallized skills (${entries.length}):`)];
|
|
5085
|
+
for (const entry of entries) {
|
|
5086
|
+
const date = entry.createdAt.slice(0, 10);
|
|
5087
|
+
lines.push(` ${pc5.cyan(entry.name)} (${date}, conf ${entry.confidence})`);
|
|
5088
|
+
lines.push(pc5.dim(` triggers: ${entry.triggers.join(", ")}`));
|
|
5089
|
+
}
|
|
5090
|
+
return { handled: true, output: lines.join("\n") };
|
|
5091
|
+
} catch {
|
|
5092
|
+
return { handled: true, output: pc5.dim("No crystallized skills yet.") };
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
const content = readEcosystemFile(path17.join(home2, ".askill", "skills.md"), "skills (askill)");
|
|
5096
|
+
return { handled: true, output: content };
|
|
5097
|
+
}
|
|
5098
|
+
if (action === "crystallize") {
|
|
5099
|
+
const pmDir = path17.join(os16.homedir(), ".acore", "postmortems");
|
|
5100
|
+
try {
|
|
5101
|
+
const files = fs17.readdirSync(pmDir);
|
|
5102
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json")).sort().reverse();
|
|
5103
|
+
if (jsonFiles.length === 0) {
|
|
5104
|
+
return {
|
|
5105
|
+
handled: true,
|
|
5106
|
+
output: pc5.dim("No post-mortems found. Run a session that triggers a post-mortem first.")
|
|
5107
|
+
};
|
|
5108
|
+
}
|
|
5109
|
+
const latest = jsonFiles[0];
|
|
5110
|
+
const content = fs17.readFileSync(path17.join(pmDir, latest), "utf-8");
|
|
5111
|
+
const report = JSON.parse(content);
|
|
5112
|
+
if (!report.crystallizationCandidates || report.crystallizationCandidates.length === 0) {
|
|
5113
|
+
return {
|
|
5114
|
+
handled: true,
|
|
5115
|
+
output: pc5.dim(`No crystallization candidates in the most recent post-mortem (${latest}). Run a longer session or wait for the next auto-postmortem.`)
|
|
5116
|
+
};
|
|
5117
|
+
}
|
|
5118
|
+
const skillsMdPath = path17.join(os16.homedir(), ".askill", "skills.md");
|
|
5119
|
+
const logPath = path17.join(os16.homedir(), ".aman-agent", "crystallization-log.json");
|
|
5120
|
+
const postmortemFilename = latest.replace(/\.json$/, ".md");
|
|
5121
|
+
const lines = [
|
|
5122
|
+
pc5.bold(`Found ${report.crystallizationCandidates.length} candidate(s) in ${latest}:`)
|
|
5123
|
+
];
|
|
5124
|
+
let written = 0;
|
|
5125
|
+
for (const raw of report.crystallizationCandidates) {
|
|
5126
|
+
const candidate = validateCandidate(raw);
|
|
5127
|
+
if (!candidate) {
|
|
5128
|
+
const rawName = raw.name ?? "unknown";
|
|
5129
|
+
lines.push(pc5.dim(` \u2298 ${rawName} \u2014 failed validation`));
|
|
5130
|
+
continue;
|
|
5131
|
+
}
|
|
5132
|
+
const result = await writeSkillToFile(candidate, skillsMdPath, postmortemFilename);
|
|
5133
|
+
if (result.written) {
|
|
5134
|
+
written++;
|
|
5135
|
+
lines.push(pc5.green(` \u2713 Crystallized: ${candidate.name}`));
|
|
5136
|
+
await appendCrystallizationLog(
|
|
5137
|
+
{
|
|
5138
|
+
name: candidate.name,
|
|
5139
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5140
|
+
fromPostmortem: postmortemFilename,
|
|
5141
|
+
confidence: candidate.confidence,
|
|
5142
|
+
triggers: candidate.triggers
|
|
5143
|
+
},
|
|
5144
|
+
logPath
|
|
5145
|
+
);
|
|
5146
|
+
} else {
|
|
5147
|
+
lines.push(pc5.yellow(` \u2298 ${candidate.name} \u2014 ${result.reason}`));
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
if (written > 0) {
|
|
5151
|
+
lines.push("");
|
|
5152
|
+
lines.push(pc5.dim(`Crystallized skills will auto-activate in your next session.`));
|
|
5153
|
+
}
|
|
5154
|
+
return { handled: true, output: lines.join("\n") };
|
|
5155
|
+
} catch (err) {
|
|
5156
|
+
return {
|
|
5157
|
+
handled: true,
|
|
5158
|
+
output: pc5.red(`Failed to load post-mortems: ${err instanceof Error ? err.message : String(err)}`)
|
|
5159
|
+
};
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
4333
5162
|
if (action === "help") {
|
|
4334
5163
|
return { handled: true, output: [
|
|
4335
5164
|
pc5.bold("Skills commands:"),
|
|
4336
|
-
` ${pc5.cyan("/skills")}
|
|
4337
|
-
` ${pc5.cyan("/skills install")} <name>
|
|
4338
|
-
` ${pc5.cyan("/skills uninstall")} <name>
|
|
4339
|
-
` ${pc5.cyan("/skills search")} <query>
|
|
5165
|
+
` ${pc5.cyan("/skills")} View installed skills`,
|
|
5166
|
+
` ${pc5.cyan("/skills install")} <name> Install a skill`,
|
|
5167
|
+
` ${pc5.cyan("/skills uninstall")} <name> Uninstall a skill`,
|
|
5168
|
+
` ${pc5.cyan("/skills search")} <query> Search skills by name/description`,
|
|
5169
|
+
` ${pc5.cyan("/skills crystallize")} Crystallize skills from most recent post-mortem`,
|
|
5170
|
+
` ${pc5.cyan("/skills list --auto")} List crystallized (auto-created) skills`
|
|
4340
5171
|
].join("\n") };
|
|
4341
5172
|
}
|
|
4342
5173
|
return { handled: true, output: pc5.yellow(`Unknown action: /skills ${action}. Try /skills --help`) };
|
|
4343
5174
|
}
|
|
4344
5175
|
async function handleEvalCommand(action, args, ctx) {
|
|
4345
|
-
const home2 =
|
|
5176
|
+
const home2 = os16.homedir();
|
|
4346
5177
|
if (!action) {
|
|
4347
|
-
const content = readEcosystemFile(
|
|
5178
|
+
const content = readEcosystemFile(path17.join(home2, ".aeval", "eval.md"), "evaluation (aeval)");
|
|
4348
5179
|
return { handled: true, output: content };
|
|
4349
5180
|
}
|
|
4350
5181
|
if (action === "milestone") {
|
|
@@ -4356,11 +5187,11 @@ async function handleEvalCommand(action, args, ctx) {
|
|
|
4356
5187
|
return { handled: true, output };
|
|
4357
5188
|
}
|
|
4358
5189
|
if (action === "report") {
|
|
4359
|
-
const evalFile =
|
|
4360
|
-
if (!
|
|
5190
|
+
const evalFile = path17.join(home2, ".aeval", "eval.md");
|
|
5191
|
+
if (!fs17.existsSync(evalFile)) {
|
|
4361
5192
|
return { handled: true, output: pc5.dim("No eval report found. Log milestones with /eval milestone <text>.") };
|
|
4362
5193
|
}
|
|
4363
|
-
const content =
|
|
5194
|
+
const content = fs17.readFileSync(evalFile, "utf-8").trim();
|
|
4364
5195
|
return { handled: true, output: [pc5.bold("Eval Report"), "", content].join("\n") };
|
|
4365
5196
|
}
|
|
4366
5197
|
return { handled: true, output: pc5.yellow(`Unknown action: /eval ${action}. Use /eval, /eval report, or /eval milestone <text>.`) };
|
|
@@ -4852,7 +5683,7 @@ function handleHelp() {
|
|
|
4852
5683
|
` ${pc5.cyan("/rules")} View rules [add|remove|toggle ...]`,
|
|
4853
5684
|
` ${pc5.cyan("/workflows")} View workflows [add|remove ...]`,
|
|
4854
5685
|
` ${pc5.cyan("/akit")} Manage tools [add|remove <tool>]`,
|
|
4855
|
-
` ${pc5.cyan("/skills")} View skills [install|uninstall
|
|
5686
|
+
` ${pc5.cyan("/skills")} View skills [install|uninstall|crystallize|list --auto]`,
|
|
4856
5687
|
` ${pc5.cyan("/eval")} View evaluation [milestone ...]`,
|
|
4857
5688
|
` ${pc5.cyan("/memory")} View recent memories [search|fts|since|stats|export|clear|timeline]`,
|
|
4858
5689
|
` ${pc5.cyan("/reminder")} Manage reminders [set|check|done]`,
|
|
@@ -4884,10 +5715,10 @@ function handleSave() {
|
|
|
4884
5715
|
}
|
|
4885
5716
|
function handleReset(action) {
|
|
4886
5717
|
const dirs = {
|
|
4887
|
-
config:
|
|
4888
|
-
memory:
|
|
4889
|
-
identity:
|
|
4890
|
-
rules:
|
|
5718
|
+
config: path17.join(os16.homedir(), ".aman-agent"),
|
|
5719
|
+
memory: path17.join(os16.homedir(), ".amem"),
|
|
5720
|
+
identity: path17.join(os16.homedir(), ".acore"),
|
|
5721
|
+
rules: path17.join(os16.homedir(), ".arules")
|
|
4891
5722
|
};
|
|
4892
5723
|
if (action === "help" || !action) {
|
|
4893
5724
|
return {
|
|
@@ -4912,15 +5743,15 @@ function handleReset(action) {
|
|
|
4912
5743
|
const removed = [];
|
|
4913
5744
|
for (const target of targets) {
|
|
4914
5745
|
const dir = dirs[target];
|
|
4915
|
-
if (
|
|
4916
|
-
|
|
5746
|
+
if (fs17.existsSync(dir)) {
|
|
5747
|
+
fs17.rmSync(dir, { recursive: true, force: true });
|
|
4917
5748
|
removed.push(target);
|
|
4918
5749
|
}
|
|
4919
5750
|
}
|
|
4920
5751
|
if (targets.includes("config")) {
|
|
4921
5752
|
const configDir = dirs.config;
|
|
4922
|
-
|
|
4923
|
-
|
|
5753
|
+
fs17.mkdirSync(configDir, { recursive: true });
|
|
5754
|
+
fs17.writeFileSync(path17.join(configDir, ".reconfig"), "", "utf-8");
|
|
4924
5755
|
}
|
|
4925
5756
|
if (removed.length === 0) {
|
|
4926
5757
|
return { handled: true, output: pc5.dim("Nothing to reset \u2014 directories don't exist.") };
|
|
@@ -4937,7 +5768,7 @@ function handleReset(action) {
|
|
|
4937
5768
|
function handleUpdate() {
|
|
4938
5769
|
try {
|
|
4939
5770
|
const current = execFileSync3("npm", ["view", "@aman_asmuei/aman-agent", "version"], { encoding: "utf-8" }).trim();
|
|
4940
|
-
const local = true ? "0.
|
|
5771
|
+
const local = true ? "0.27.0" : "unknown";
|
|
4941
5772
|
if (current === local) {
|
|
4942
5773
|
return { handled: true, output: `${pc5.green("Up to date")} \u2014 v${local}` };
|
|
4943
5774
|
}
|
|
@@ -4981,11 +5812,11 @@ function handleExportCommand() {
|
|
|
4981
5812
|
return { handled: true, exportConversation: true };
|
|
4982
5813
|
}
|
|
4983
5814
|
function handleDebugCommand() {
|
|
4984
|
-
const logPath =
|
|
4985
|
-
if (!
|
|
5815
|
+
const logPath = path17.join(os16.homedir(), ".aman-agent", "debug.log");
|
|
5816
|
+
if (!fs17.existsSync(logPath)) {
|
|
4986
5817
|
return { handled: true, output: pc5.dim("No debug log found.") };
|
|
4987
5818
|
}
|
|
4988
|
-
const content =
|
|
5819
|
+
const content = fs17.readFileSync(logPath, "utf-8");
|
|
4989
5820
|
const lines = content.trim().split("\n");
|
|
4990
5821
|
const last20 = lines.slice(-20).join("\n");
|
|
4991
5822
|
return { handled: true, output: pc5.bold("Debug Log (last 20 entries):\n") + pc5.dim(last20) };
|
|
@@ -5183,7 +6014,7 @@ ${result.response}`
|
|
|
5183
6014
|
};
|
|
5184
6015
|
}
|
|
5185
6016
|
function handleProfileCommand(action, args) {
|
|
5186
|
-
const profilesDir =
|
|
6017
|
+
const profilesDir = path17.join(os16.homedir(), ".acore", "profiles");
|
|
5187
6018
|
if (action === "me") {
|
|
5188
6019
|
const user = loadUserIdentity();
|
|
5189
6020
|
if (!user) {
|
|
@@ -5251,8 +6082,8 @@ ${pc5.dim("Edit with: /profile edit")}` };
|
|
|
5251
6082
|
};
|
|
5252
6083
|
}
|
|
5253
6084
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
5254
|
-
const profileDir =
|
|
5255
|
-
if (
|
|
6085
|
+
const profileDir = path17.join(profilesDir, slug);
|
|
6086
|
+
if (fs17.existsSync(profileDir)) {
|
|
5256
6087
|
return { handled: true, output: pc5.yellow(`Profile already exists: ${slug}`) };
|
|
5257
6088
|
}
|
|
5258
6089
|
const builtIn = BUILT_IN_PROFILES.find((t) => t.name === slug);
|
|
@@ -5268,16 +6099,16 @@ ${pc5.dim("Edit with: /profile edit")}` };
|
|
|
5268
6099
|
Use: aman-agent --profile ${slug}`
|
|
5269
6100
|
};
|
|
5270
6101
|
}
|
|
5271
|
-
|
|
5272
|
-
const globalCore =
|
|
5273
|
-
if (
|
|
5274
|
-
let content =
|
|
6102
|
+
fs17.mkdirSync(profileDir, { recursive: true });
|
|
6103
|
+
const globalCore = path17.join(os16.homedir(), ".acore", "core.md");
|
|
6104
|
+
if (fs17.existsSync(globalCore)) {
|
|
6105
|
+
let content = fs17.readFileSync(globalCore, "utf-8");
|
|
5275
6106
|
const aiName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
5276
6107
|
content = content.replace(/^# .+$/m, `# ${aiName}`);
|
|
5277
|
-
|
|
6108
|
+
fs17.writeFileSync(path17.join(profileDir, "core.md"), content, "utf-8");
|
|
5278
6109
|
} else {
|
|
5279
6110
|
const aiName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
5280
|
-
|
|
6111
|
+
fs17.writeFileSync(path17.join(profileDir, "core.md"), `# ${aiName}
|
|
5281
6112
|
|
|
5282
6113
|
## Identity
|
|
5283
6114
|
- Role: ${aiName} is your AI companion
|
|
@@ -5290,7 +6121,7 @@ ${pc5.dim("Edit with: /profile edit")}` };
|
|
|
5290
6121
|
return {
|
|
5291
6122
|
handled: true,
|
|
5292
6123
|
output: pc5.green(`Profile created: ${slug}`) + `
|
|
5293
|
-
Edit: ${
|
|
6124
|
+
Edit: ${path17.join(profileDir, "core.md")}
|
|
5294
6125
|
Use: aman-agent --profile ${slug}
|
|
5295
6126
|
|
|
5296
6127
|
${pc5.dim("Add rules.md or skills.md for profile-specific overrides.")}`
|
|
@@ -5299,9 +6130,9 @@ ${pc5.dim("Edit with: /profile edit")}` };
|
|
|
5299
6130
|
case "show": {
|
|
5300
6131
|
const name = args[0];
|
|
5301
6132
|
if (!name) return { handled: true, output: pc5.yellow("Usage: /profile show <name>") };
|
|
5302
|
-
const profileDir =
|
|
5303
|
-
if (!
|
|
5304
|
-
const files =
|
|
6133
|
+
const profileDir = path17.join(profilesDir, name);
|
|
6134
|
+
if (!fs17.existsSync(profileDir)) return { handled: true, output: pc5.red(`Profile not found: ${name}`) };
|
|
6135
|
+
const files = fs17.readdirSync(profileDir).filter((f) => f.endsWith(".md"));
|
|
5305
6136
|
const lines = files.map((f) => ` ${f}`);
|
|
5306
6137
|
return { handled: true, output: `Profile: ${pc5.bold(name)}
|
|
5307
6138
|
Files:
|
|
@@ -5310,9 +6141,9 @@ ${lines.join("\n")}` };
|
|
|
5310
6141
|
case "delete": {
|
|
5311
6142
|
const name = args[0];
|
|
5312
6143
|
if (!name) return { handled: true, output: pc5.yellow("Usage: /profile delete <name>") };
|
|
5313
|
-
const profileDir =
|
|
5314
|
-
if (!
|
|
5315
|
-
|
|
6144
|
+
const profileDir = path17.join(profilesDir, name);
|
|
6145
|
+
if (!fs17.existsSync(profileDir)) return { handled: true, output: pc5.red(`Profile not found: ${name}`) };
|
|
6146
|
+
fs17.rmSync(profileDir, { recursive: true });
|
|
5316
6147
|
return { handled: true, output: pc5.dim(`Profile deleted: ${name}`) };
|
|
5317
6148
|
}
|
|
5318
6149
|
case "help":
|
|
@@ -5530,10 +6361,10 @@ function handleShowcaseCommand(action, args) {
|
|
|
5530
6361
|
Or place it as a sibling directory to aman-agent.`
|
|
5531
6362
|
};
|
|
5532
6363
|
}
|
|
5533
|
-
const corePath =
|
|
6364
|
+
const corePath = path17.join(os16.homedir(), ".acore", "core.md");
|
|
5534
6365
|
let currentShowcase = null;
|
|
5535
|
-
if (
|
|
5536
|
-
const content =
|
|
6366
|
+
if (fs17.existsSync(corePath)) {
|
|
6367
|
+
const content = fs17.readFileSync(corePath, "utf-8");
|
|
5537
6368
|
const nameMatch = content.match(/^# (.+)/m);
|
|
5538
6369
|
if (nameMatch) {
|
|
5539
6370
|
const coreName = nameMatch[1].trim().toLowerCase();
|
|
@@ -5920,9 +6751,10 @@ ${summaryParts.slice(0, 20).join("\n")}
|
|
|
5920
6751
|
}
|
|
5921
6752
|
|
|
5922
6753
|
// src/skill-engine.ts
|
|
5923
|
-
import
|
|
5924
|
-
import
|
|
5925
|
-
import
|
|
6754
|
+
import fs18 from "fs";
|
|
6755
|
+
import fsp from "fs/promises";
|
|
6756
|
+
import path18 from "path";
|
|
6757
|
+
import os17 from "os";
|
|
5926
6758
|
var SKILL_TRIGGERS = {
|
|
5927
6759
|
testing: ["test", "spec", "coverage", "tdd", "jest", "vitest", "mocha", "assert", "mock", "stub", "fixture", "e2e", "integration test", "unit test"],
|
|
5928
6760
|
"api-design": ["api", "endpoint", "rest", "graphql", "route", "controller", "middleware", "http", "request", "response", "status code", "pagination"],
|
|
@@ -5937,20 +6769,34 @@ var SKILL_TRIGGERS = {
|
|
|
5937
6769
|
typescript: ["typescript", "type", "interface", "generic", "infer", "utility type", "zod", "discriminated union", "type guard", "as const"],
|
|
5938
6770
|
accessibility: ["accessibility", "a11y", "aria", "screen reader", "wcag", "semantic html", "tab order", "focus", "contrast"]
|
|
5939
6771
|
};
|
|
5940
|
-
|
|
6772
|
+
async function loadRuntimeTriggers(skillsMdPath) {
|
|
6773
|
+
try {
|
|
6774
|
+
const content = await fsp.readFile(skillsMdPath, "utf-8");
|
|
6775
|
+
const skills = extractSkillsWithMarkers(content);
|
|
6776
|
+
const result = /* @__PURE__ */ new Map();
|
|
6777
|
+
for (const [name, marker] of skills) {
|
|
6778
|
+
result.set(name, marker.triggers);
|
|
6779
|
+
}
|
|
6780
|
+
return result;
|
|
6781
|
+
} catch (err) {
|
|
6782
|
+
log.debug("skill-engine", "loadRuntimeTriggers failed", err);
|
|
6783
|
+
return /* @__PURE__ */ new Map();
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
var LEVEL_FILE = path18.join(os17.homedir(), ".aman-agent", "skill-levels.json");
|
|
5941
6787
|
function loadSkillLevels() {
|
|
5942
6788
|
try {
|
|
5943
|
-
if (
|
|
5944
|
-
return JSON.parse(
|
|
6789
|
+
if (fs18.existsSync(LEVEL_FILE)) {
|
|
6790
|
+
return JSON.parse(fs18.readFileSync(LEVEL_FILE, "utf-8"));
|
|
5945
6791
|
}
|
|
5946
6792
|
} catch {
|
|
5947
6793
|
}
|
|
5948
6794
|
return {};
|
|
5949
6795
|
}
|
|
5950
6796
|
function saveSkillLevels(levels) {
|
|
5951
|
-
const dir =
|
|
5952
|
-
if (!
|
|
5953
|
-
|
|
6797
|
+
const dir = path18.dirname(LEVEL_FILE);
|
|
6798
|
+
if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
|
|
6799
|
+
fs18.writeFileSync(LEVEL_FILE, JSON.stringify(levels, null, 2), "utf-8");
|
|
5954
6800
|
}
|
|
5955
6801
|
function computeLevel(activations) {
|
|
5956
6802
|
if (activations >= 50) return { level: 5, label: "Expert" };
|
|
@@ -5969,20 +6815,28 @@ function recordActivation(skillName) {
|
|
|
5969
6815
|
saveSkillLevels(levels);
|
|
5970
6816
|
return computeLevel(levels[skillName].activations);
|
|
5971
6817
|
}
|
|
5972
|
-
function matchSkills(userInput, installedSkillNames) {
|
|
6818
|
+
function matchSkills(userInput, installedSkillNames, runtimeTriggers = /* @__PURE__ */ new Map()) {
|
|
5973
6819
|
const input = userInput.toLowerCase();
|
|
5974
|
-
const matched =
|
|
6820
|
+
const matched = /* @__PURE__ */ new Set();
|
|
5975
6821
|
for (const skillName of installedSkillNames) {
|
|
5976
6822
|
const triggers = SKILL_TRIGGERS[skillName];
|
|
5977
6823
|
if (!triggers) continue;
|
|
5978
6824
|
for (const trigger of triggers) {
|
|
5979
6825
|
if (input.includes(trigger)) {
|
|
5980
|
-
matched.
|
|
6826
|
+
matched.add(skillName);
|
|
6827
|
+
break;
|
|
6828
|
+
}
|
|
6829
|
+
}
|
|
6830
|
+
}
|
|
6831
|
+
for (const [skillName, triggers] of runtimeTriggers) {
|
|
6832
|
+
for (const trigger of triggers) {
|
|
6833
|
+
if (input.includes(trigger)) {
|
|
6834
|
+
matched.add(skillName);
|
|
5981
6835
|
break;
|
|
5982
6836
|
}
|
|
5983
6837
|
}
|
|
5984
6838
|
}
|
|
5985
|
-
return matched;
|
|
6839
|
+
return Array.from(matched);
|
|
5986
6840
|
}
|
|
5987
6841
|
function formatSkillContext(skillName, skillContent, level) {
|
|
5988
6842
|
let depthHint;
|
|
@@ -6006,8 +6860,10 @@ async function autoTriggerSkills(userInput, mcpManager) {
|
|
|
6006
6860
|
const result = await mcpManager.callTool("skill_list", {});
|
|
6007
6861
|
const skills = JSON.parse(result);
|
|
6008
6862
|
const installed = skills.filter((s) => s.installed).map((s) => s.name);
|
|
6009
|
-
|
|
6010
|
-
const
|
|
6863
|
+
const skillsMdPath = path18.join(os17.homedir(), ".askill", "skills.md");
|
|
6864
|
+
const runtimeTriggers = await loadRuntimeTriggers(skillsMdPath);
|
|
6865
|
+
if (installed.length === 0 && runtimeTriggers.size === 0) return "";
|
|
6866
|
+
const matched = matchSkills(userInput, installed, runtimeTriggers);
|
|
6011
6867
|
if (matched.length === 0) return "";
|
|
6012
6868
|
const blocks = [];
|
|
6013
6869
|
for (const skillName of matched.slice(0, 2)) {
|
|
@@ -6563,9 +7419,9 @@ function humanizeError(message) {
|
|
|
6563
7419
|
}
|
|
6564
7420
|
|
|
6565
7421
|
// src/hints.ts
|
|
6566
|
-
import
|
|
6567
|
-
import
|
|
6568
|
-
import
|
|
7422
|
+
import fs19 from "fs";
|
|
7423
|
+
import path19 from "path";
|
|
7424
|
+
import os18 from "os";
|
|
6569
7425
|
var HINTS = [
|
|
6570
7426
|
{
|
|
6571
7427
|
id: "eval",
|
|
@@ -6603,11 +7459,11 @@ function getHint(state, ctx) {
|
|
|
6603
7459
|
}
|
|
6604
7460
|
return null;
|
|
6605
7461
|
}
|
|
6606
|
-
var HINTS_FILE =
|
|
7462
|
+
var HINTS_FILE = path19.join(os18.homedir(), ".aman-agent", "hints-seen.json");
|
|
6607
7463
|
function loadShownHints() {
|
|
6608
7464
|
try {
|
|
6609
|
-
if (
|
|
6610
|
-
const data = JSON.parse(
|
|
7465
|
+
if (fs19.existsSync(HINTS_FILE)) {
|
|
7466
|
+
const data = JSON.parse(fs19.readFileSync(HINTS_FILE, "utf-8"));
|
|
6611
7467
|
return new Set(Array.isArray(data) ? data : []);
|
|
6612
7468
|
}
|
|
6613
7469
|
} catch {
|
|
@@ -6616,9 +7472,9 @@ function loadShownHints() {
|
|
|
6616
7472
|
}
|
|
6617
7473
|
function saveShownHints(shown) {
|
|
6618
7474
|
try {
|
|
6619
|
-
const dir =
|
|
6620
|
-
|
|
6621
|
-
|
|
7475
|
+
const dir = path19.dirname(HINTS_FILE);
|
|
7476
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
7477
|
+
fs19.writeFileSync(HINTS_FILE, JSON.stringify([...shown]), "utf-8");
|
|
6622
7478
|
} catch {
|
|
6623
7479
|
}
|
|
6624
7480
|
}
|
|
@@ -6885,9 +7741,9 @@ ${task.result}`
|
|
|
6885
7741
|
}
|
|
6886
7742
|
if (cmdResult.exportConversation) {
|
|
6887
7743
|
try {
|
|
6888
|
-
const exportDir =
|
|
6889
|
-
|
|
6890
|
-
const exportPath =
|
|
7744
|
+
const exportDir = path20.join(os19.homedir(), ".aman-agent", "exports");
|
|
7745
|
+
fs20.mkdirSync(exportDir, { recursive: true });
|
|
7746
|
+
const exportPath = path20.join(exportDir, `${sessionId}.md`);
|
|
6891
7747
|
const lines = [
|
|
6892
7748
|
`# Conversation \u2014 ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
|
|
6893
7749
|
`**Model:** ${model}`,
|
|
@@ -6901,7 +7757,7 @@ ${task.result}`
|
|
|
6901
7757
|
lines.push(`${label} ${msg.content}`, "");
|
|
6902
7758
|
}
|
|
6903
7759
|
}
|
|
6904
|
-
|
|
7760
|
+
fs20.writeFileSync(exportPath, lines.join("\n"), "utf-8");
|
|
6905
7761
|
console.log(pc7.green(`Exported to ${exportPath}`));
|
|
6906
7762
|
} catch {
|
|
6907
7763
|
console.log(pc7.red("Failed to export conversation."));
|
|
@@ -7027,25 +7883,25 @@ ${knowledgeItem.content}
|
|
|
7027
7883
|
for (const match of filePathMatches) {
|
|
7028
7884
|
let filePath = match[1];
|
|
7029
7885
|
if (filePath.startsWith("~/")) {
|
|
7030
|
-
filePath =
|
|
7886
|
+
filePath = path20.join(os19.homedir(), filePath.slice(2));
|
|
7031
7887
|
}
|
|
7032
|
-
if (!
|
|
7033
|
-
const ext =
|
|
7888
|
+
if (!fs20.existsSync(filePath) || !fs20.statSync(filePath).isFile()) continue;
|
|
7889
|
+
const ext = path20.extname(filePath).toLowerCase();
|
|
7034
7890
|
if (imageExts.has(ext)) {
|
|
7035
7891
|
try {
|
|
7036
|
-
const stat =
|
|
7892
|
+
const stat = fs20.statSync(filePath);
|
|
7037
7893
|
if (stat.size > maxImageBytes) {
|
|
7038
|
-
process.stdout.write(pc7.yellow(` [skipped: ${
|
|
7894
|
+
process.stdout.write(pc7.yellow(` [skipped: ${path20.basename(filePath)} \u2014 exceeds 20MB limit]
|
|
7039
7895
|
`));
|
|
7040
7896
|
continue;
|
|
7041
7897
|
}
|
|
7042
|
-
const data =
|
|
7898
|
+
const data = fs20.readFileSync(filePath).toString("base64");
|
|
7043
7899
|
const mediaType = mimeMap[ext] || "image/png";
|
|
7044
7900
|
imageBlocks.push({
|
|
7045
7901
|
type: "image",
|
|
7046
7902
|
source: { type: "base64", media_type: mediaType, data }
|
|
7047
7903
|
});
|
|
7048
|
-
process.stdout.write(pc7.dim(` [attached image: ${
|
|
7904
|
+
process.stdout.write(pc7.dim(` [attached image: ${path20.basename(filePath)} (${(stat.size / 1024).toFixed(1)}KB)]
|
|
7049
7905
|
`));
|
|
7050
7906
|
} catch {
|
|
7051
7907
|
process.stdout.write(pc7.dim(` [could not read image: ${filePath}]
|
|
@@ -7053,7 +7909,7 @@ ${knowledgeItem.content}
|
|
|
7053
7909
|
}
|
|
7054
7910
|
} else if (textExts.has(ext) || ext === "") {
|
|
7055
7911
|
try {
|
|
7056
|
-
const content =
|
|
7912
|
+
const content = fs20.readFileSync(filePath, "utf-8");
|
|
7057
7913
|
const maxChars = 5e4;
|
|
7058
7914
|
const trimmed = content.length > maxChars ? content.slice(0, maxChars) + `
|
|
7059
7915
|
|
|
@@ -7063,7 +7919,7 @@ ${knowledgeItem.content}
|
|
|
7063
7919
|
<file path="${filePath}" size="${content.length} chars">
|
|
7064
7920
|
${trimmed}
|
|
7065
7921
|
</file>`;
|
|
7066
|
-
process.stdout.write(pc7.dim(` [attached: ${
|
|
7922
|
+
process.stdout.write(pc7.dim(` [attached: ${path20.basename(filePath)} (${(content.length / 1024).toFixed(1)}KB)]
|
|
7067
7923
|
`));
|
|
7068
7924
|
} catch {
|
|
7069
7925
|
process.stdout.write(pc7.dim(` [could not read: ${filePath}]
|
|
@@ -7072,7 +7928,7 @@ ${trimmed}
|
|
|
7072
7928
|
} else if (docExts.has(ext)) {
|
|
7073
7929
|
if (mcpManager) {
|
|
7074
7930
|
try {
|
|
7075
|
-
process.stdout.write(pc7.dim(` [converting: ${
|
|
7931
|
+
process.stdout.write(pc7.dim(` [converting: ${path20.basename(filePath)}...]
|
|
7076
7932
|
`));
|
|
7077
7933
|
const converted = await mcpManager.callTool("doc_convert", { path: filePath });
|
|
7078
7934
|
if (converted && !converted.startsWith("Error") && !converted.includes("Could not convert")) {
|
|
@@ -7081,7 +7937,7 @@ ${trimmed}
|
|
|
7081
7937
|
<file path="${filePath}" format="${ext}">
|
|
7082
7938
|
${converted.slice(0, 5e4)}
|
|
7083
7939
|
</file>`;
|
|
7084
|
-
process.stdout.write(pc7.dim(` [attached: ${
|
|
7940
|
+
process.stdout.write(pc7.dim(` [attached: ${path20.basename(filePath)} (converted from ${ext})]
|
|
7085
7941
|
`));
|
|
7086
7942
|
} else {
|
|
7087
7943
|
textContent += `
|
|
@@ -7093,7 +7949,7 @@ ${converted}
|
|
|
7093
7949
|
`));
|
|
7094
7950
|
}
|
|
7095
7951
|
} catch {
|
|
7096
|
-
process.stdout.write(pc7.dim(` [could not convert: ${
|
|
7952
|
+
process.stdout.write(pc7.dim(` [could not convert: ${path20.basename(filePath)}]
|
|
7097
7953
|
`));
|
|
7098
7954
|
}
|
|
7099
7955
|
} else {
|
|
@@ -7397,7 +8253,7 @@ ${result2.response}` : `[${input2.profile}] failed: ${result2.error}`;
|
|
|
7397
8253
|
}
|
|
7398
8254
|
if (hooksConfig?.featureHints) {
|
|
7399
8255
|
hintState.turnCount++;
|
|
7400
|
-
const hasWorkflows =
|
|
8256
|
+
const hasWorkflows = fs20.existsSync(path20.join(os19.homedir(), ".aflow", "flow.md"));
|
|
7401
8257
|
const memoryCount = memoryTokens > 0 ? Math.floor(memoryTokens / 5) : 0;
|
|
7402
8258
|
const hint = getHint(hintState, { hasWorkflows, memoryCount });
|
|
7403
8259
|
if (hint) {
|
|
@@ -7443,9 +8299,9 @@ async function saveConversationToMemory(messages, sessionId) {
|
|
|
7443
8299
|
}
|
|
7444
8300
|
|
|
7445
8301
|
// src/index.ts
|
|
7446
|
-
import
|
|
7447
|
-
import
|
|
7448
|
-
import
|
|
8302
|
+
import fs21 from "fs";
|
|
8303
|
+
import path21 from "path";
|
|
8304
|
+
import os20 from "os";
|
|
7449
8305
|
|
|
7450
8306
|
// src/presets.ts
|
|
7451
8307
|
var PRESETS = {
|
|
@@ -7554,9 +8410,9 @@ ${wfSections}`;
|
|
|
7554
8410
|
|
|
7555
8411
|
// src/index.ts
|
|
7556
8412
|
async function autoDetectConfig() {
|
|
7557
|
-
const reconfigMarker =
|
|
7558
|
-
if (
|
|
7559
|
-
|
|
8413
|
+
const reconfigMarker = path21.join(os20.homedir(), ".aman-agent", ".reconfig");
|
|
8414
|
+
if (fs21.existsSync(reconfigMarker)) {
|
|
8415
|
+
fs21.unlinkSync(reconfigMarker);
|
|
7560
8416
|
return null;
|
|
7561
8417
|
}
|
|
7562
8418
|
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
@@ -7585,11 +8441,11 @@ async function autoDetectConfig() {
|
|
|
7585
8441
|
return null;
|
|
7586
8442
|
}
|
|
7587
8443
|
function bootstrapEcosystem() {
|
|
7588
|
-
const home2 =
|
|
7589
|
-
const corePath =
|
|
7590
|
-
if (
|
|
7591
|
-
|
|
7592
|
-
|
|
8444
|
+
const home2 = os20.homedir();
|
|
8445
|
+
const corePath = path21.join(home2, ".acore", "core.md");
|
|
8446
|
+
if (fs21.existsSync(corePath)) return false;
|
|
8447
|
+
fs21.mkdirSync(path21.join(home2, ".acore"), { recursive: true });
|
|
8448
|
+
fs21.writeFileSync(corePath, [
|
|
7593
8449
|
"# Aman",
|
|
7594
8450
|
"",
|
|
7595
8451
|
"## Personality",
|
|
@@ -7601,11 +8457,11 @@ function bootstrapEcosystem() {
|
|
|
7601
8457
|
"## Session",
|
|
7602
8458
|
"_New companion \u2014 no prior sessions._"
|
|
7603
8459
|
].join("\n"), "utf-8");
|
|
7604
|
-
const rulesDir =
|
|
7605
|
-
const rulesPath =
|
|
7606
|
-
if (!
|
|
7607
|
-
|
|
7608
|
-
|
|
8460
|
+
const rulesDir = path21.join(home2, ".arules");
|
|
8461
|
+
const rulesPath = path21.join(rulesDir, "rules.md");
|
|
8462
|
+
if (!fs21.existsSync(rulesPath)) {
|
|
8463
|
+
fs21.mkdirSync(rulesDir, { recursive: true });
|
|
8464
|
+
fs21.writeFileSync(rulesPath, [
|
|
7609
8465
|
"# Guardrails",
|
|
7610
8466
|
"",
|
|
7611
8467
|
"## safety",
|
|
@@ -7617,22 +8473,22 @@ function bootstrapEcosystem() {
|
|
|
7617
8473
|
"- Respect the user's preferences stored in memory"
|
|
7618
8474
|
].join("\n"), "utf-8");
|
|
7619
8475
|
}
|
|
7620
|
-
const flowDir =
|
|
7621
|
-
const flowPath =
|
|
7622
|
-
if (!
|
|
7623
|
-
|
|
7624
|
-
|
|
8476
|
+
const flowDir = path21.join(home2, ".aflow");
|
|
8477
|
+
const flowPath = path21.join(flowDir, "flow.md");
|
|
8478
|
+
if (!fs21.existsSync(flowPath)) {
|
|
8479
|
+
fs21.mkdirSync(flowDir, { recursive: true });
|
|
8480
|
+
fs21.writeFileSync(flowPath, "# Workflows\n\n_No workflows defined yet. Use /workflows add to create one._\n", "utf-8");
|
|
7625
8481
|
}
|
|
7626
|
-
const skillDir =
|
|
7627
|
-
const skillPath =
|
|
7628
|
-
if (!
|
|
7629
|
-
|
|
7630
|
-
|
|
8482
|
+
const skillDir = path21.join(home2, ".askill");
|
|
8483
|
+
const skillPath = path21.join(skillDir, "skills.md");
|
|
8484
|
+
if (!fs21.existsSync(skillPath)) {
|
|
8485
|
+
fs21.mkdirSync(skillDir, { recursive: true });
|
|
8486
|
+
fs21.writeFileSync(skillPath, "# Skills\n\n_No skills installed yet. Use /skills install to add domain expertise._\n", "utf-8");
|
|
7631
8487
|
}
|
|
7632
8488
|
return true;
|
|
7633
8489
|
}
|
|
7634
8490
|
var program = new Command();
|
|
7635
|
-
program.name("aman-agent").description("Your AI companion, running locally").version("0.
|
|
8491
|
+
program.name("aman-agent").description("Your AI companion, running locally").version("0.27.0").option("--model <model>", "Override LLM model").option("--budget <tokens>", "Token budget for system prompt (default: 8000)", parseInt).option("--profile <name>", "Use a specific agent profile (e.g., coder, writer, researcher)").action(async (options) => {
|
|
7636
8492
|
p3.intro(pc8.bold("aman agent") + pc8.dim(" \u2014 your AI companion"));
|
|
7637
8493
|
let config = loadConfig();
|
|
7638
8494
|
if (!config) {
|
|
@@ -7984,19 +8840,19 @@ program.command("init").description("Set up your AI companion with a guided wiza
|
|
|
7984
8840
|
});
|
|
7985
8841
|
if (p3.isCancel(preset)) process.exit(0);
|
|
7986
8842
|
const result = applyPreset(preset, name || "Aman");
|
|
7987
|
-
const home2 =
|
|
7988
|
-
|
|
7989
|
-
|
|
8843
|
+
const home2 = os20.homedir();
|
|
8844
|
+
fs21.mkdirSync(path21.join(home2, ".acore"), { recursive: true });
|
|
8845
|
+
fs21.writeFileSync(path21.join(home2, ".acore", "core.md"), result.coreMd, "utf-8");
|
|
7990
8846
|
p3.log.success(`Identity created \u2014 ${PRESETS[preset].identity.personality.split(".")[0].toLowerCase()}`);
|
|
7991
8847
|
if (result.rulesMd) {
|
|
7992
|
-
|
|
7993
|
-
|
|
8848
|
+
fs21.mkdirSync(path21.join(home2, ".arules"), { recursive: true });
|
|
8849
|
+
fs21.writeFileSync(path21.join(home2, ".arules", "rules.md"), result.rulesMd, "utf-8");
|
|
7994
8850
|
const ruleCount = (result.rulesMd.match(/^- /gm) || []).length;
|
|
7995
8851
|
p3.log.success(`${ruleCount} rules set`);
|
|
7996
8852
|
}
|
|
7997
8853
|
if (result.flowMd) {
|
|
7998
|
-
|
|
7999
|
-
|
|
8854
|
+
fs21.mkdirSync(path21.join(home2, ".aflow"), { recursive: true });
|
|
8855
|
+
fs21.writeFileSync(path21.join(home2, ".aflow", "flow.md"), result.flowMd, "utf-8");
|
|
8000
8856
|
const wfCount = (result.flowMd.match(/^## /gm) || []).length;
|
|
8001
8857
|
p3.log.success(`${wfCount} workflow${wfCount > 1 ? "s" : ""} added`);
|
|
8002
8858
|
}
|