@aman_asmuei/aman-agent 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1387,18 +1387,18 @@ var McpManager = class {
1387
1387
 
1388
1388
  // src/agent.ts
1389
1389
  import * as readline from "readline";
1390
- import fs18 from "fs";
1391
- import path18 from "path";
1392
- import os17 from "os";
1390
+ import fs19 from "fs";
1391
+ import path19 from "path";
1392
+ import os18 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 fs15 from "fs";
1400
- import path15 from "path";
1401
- import os14 from "os";
1399
+ import fs16 from "fs";
1400
+ import path16 from "path";
1401
+ import os15 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 fs12 from "fs";
2365
- import path12 from "path";
2364
+ import fs13 from "fs";
2365
+ import path13 from "path";
2366
+ import os12 from "os";
2366
2367
 
2367
2368
  // src/personality.ts
2368
2369
  var FRUSTRATION_SIGNALS = [
@@ -2689,8 +2690,28 @@ Return ONLY valid JSON matching this schema (no markdown, no explanation):
2689
2690
  "decisions": ["key choices made with rationale"],
2690
2691
  "sentimentArc": "how mood evolved during session",
2691
2692
  "patterns": ["recurring behaviors worth remembering for future sessions"],
2692
- "recommendations": ["actionable suggestions for next session"]
2693
- }`;
2693
+ "recommendations": ["actionable suggestions for next session"],
2694
+ "crystallizationCandidates": [
2695
+ {
2696
+ "name": "lowercase-kebab-name",
2697
+ "description": "1-sentence description of when this would be useful",
2698
+ "triggers": ["3-8", "trigger", "keywords"],
2699
+ "approach": "1-paragraph context: when and why to use this procedure",
2700
+ "steps": ["ordered step 1", "ordered step 2"],
2701
+ "gotchas": ["common mistake 1"],
2702
+ "confidence": 0.0
2703
+ }
2704
+ ]
2705
+ }
2706
+
2707
+ CRYSTALLIZATION RULES:
2708
+ - Only suggest 0-2 candidates per session \u2014 if nothing qualifies, return an empty array
2709
+ - Only suggest REUSABLE procedures (not one-off tasks specific to today's work)
2710
+ - The user must have demonstrated the procedure in this session
2711
+ - Confidence < 0.6 \u2192 don't suggest at all
2712
+ - Skip vague things like "use library X" \u2014 that's not procedural knowledge
2713
+ - Prefer narrow specific procedures over broad generalizations
2714
+ - Trigger keywords should be highly specific (avoid generic words like "code", "fix", "the")`;
2694
2715
  async function generatePostmortemReport(sessionId, messages, session, client, obsDir) {
2695
2716
  try {
2696
2717
  const events = await readObservationEvents(sessionId, obsDir ?? defaultObservationsDir2());
@@ -2770,7 +2791,8 @@ ${obsSnapshot.join("\n")}`;
2770
2791
  topicProgression: [...new Set(topicProgression)],
2771
2792
  sentimentArc: parsed.sentimentArc ?? "",
2772
2793
  patterns: parsed.patterns ?? [],
2773
- recommendations: parsed.recommendations ?? []
2794
+ recommendations: parsed.recommendations ?? [],
2795
+ crystallizationCandidates: Array.isArray(parsed.crystallizationCandidates) ? parsed.crystallizationCandidates : void 0
2774
2796
  };
2775
2797
  } catch (err) {
2776
2798
  log.debug("postmortem", "Failed to generate post-mortem", err);
@@ -2841,6 +2863,14 @@ function formatPostmortemMarkdown(report) {
2841
2863
  report.recommendations.forEach((r) => lines.push(`- ${r}`));
2842
2864
  lines.push("");
2843
2865
  }
2866
+ if (report.crystallizationCandidates && report.crystallizationCandidates.length > 0) {
2867
+ lines.push("## Crystallization Candidates");
2868
+ report.crystallizationCandidates.forEach((c) => {
2869
+ lines.push(`- **${c.name}** (confidence ${c.confidence})`);
2870
+ lines.push(` ${c.description}`);
2871
+ });
2872
+ lines.push("");
2873
+ }
2844
2874
  return lines.join("\n");
2845
2875
  }
2846
2876
  async function savePostmortem(report, dir) {
@@ -2851,6 +2881,12 @@ async function savePostmortem(report, dir) {
2851
2881
  const filePath = path11.join(pmDir, fileName);
2852
2882
  const markdown = formatPostmortemMarkdown(report);
2853
2883
  await fs11.writeFile(filePath, markdown, "utf-8");
2884
+ const jsonPath = filePath.replace(/\.md$/, ".json");
2885
+ try {
2886
+ await fs11.writeFile(jsonPath, JSON.stringify(report, null, 2), "utf-8");
2887
+ } catch (err) {
2888
+ log.debug("postmortem", "JSON sidecar write failed", err);
2889
+ }
2854
2890
  return filePath;
2855
2891
  }
2856
2892
  async function listPostmortems(dir) {
@@ -2911,6 +2947,259 @@ ${contents.join("\n\n---\n\n")}`
2911
2947
  }
2912
2948
  }
2913
2949
 
2950
+ // src/crystallization.ts
2951
+ import fs12 from "fs/promises";
2952
+ import path12 from "path";
2953
+ var STOPWORDS = /* @__PURE__ */ new Set([
2954
+ "the",
2955
+ "and",
2956
+ "is",
2957
+ "to",
2958
+ "of",
2959
+ "a",
2960
+ "in",
2961
+ "for",
2962
+ "on",
2963
+ "with",
2964
+ "this",
2965
+ "that",
2966
+ "it",
2967
+ "as",
2968
+ "be",
2969
+ "by",
2970
+ "or",
2971
+ "at",
2972
+ "an",
2973
+ "from",
2974
+ "code",
2975
+ "fix",
2976
+ "do",
2977
+ "use",
2978
+ "make",
2979
+ "get",
2980
+ "set",
2981
+ "run",
2982
+ "we",
2983
+ "i"
2984
+ ]);
2985
+ var MAX_REJECTIONS = 100;
2986
+ var MARKER_RE = /<!--\s*aman-auto\s+([^>]+?)\s*-->/;
2987
+ function sanitizeName(input) {
2988
+ const cleaned = input.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2989
+ if (cleaned.length === 0) {
2990
+ throw new Error(`Cannot sanitize name: "${input}" produced empty result`);
2991
+ }
2992
+ return cleaned;
2993
+ }
2994
+ function validateCandidate(raw) {
2995
+ if (!raw || typeof raw !== "object") return null;
2996
+ const c = raw;
2997
+ if (typeof c.name !== "string" || c.name.trim() === "") return null;
2998
+ if (typeof c.description !== "string") return null;
2999
+ if (typeof c.approach !== "string") return null;
3000
+ if (!Array.isArray(c.triggers) || c.triggers.length === 0) return null;
3001
+ if (c.triggers.length > 10) return null;
3002
+ if (!Array.isArray(c.steps)) return null;
3003
+ if (typeof c.confidence !== "number") return null;
3004
+ if (!Number.isFinite(c.confidence)) return null;
3005
+ if (c.confidence < 0.6) return null;
3006
+ const triggers = Array.from(
3007
+ new Set(
3008
+ c.triggers.filter((t) => typeof t === "string").map((t) => t.toLowerCase().trim()).filter((t) => t.length > 0 && !STOPWORDS.has(t))
3009
+ )
3010
+ );
3011
+ if (triggers.length === 0) return null;
3012
+ let name;
3013
+ try {
3014
+ name = sanitizeName(c.name);
3015
+ } catch {
3016
+ return null;
3017
+ }
3018
+ return {
3019
+ name,
3020
+ description: c.description,
3021
+ triggers,
3022
+ approach: c.approach,
3023
+ steps: c.steps.filter((s) => typeof s === "string"),
3024
+ gotchas: Array.isArray(c.gotchas) ? c.gotchas.filter((g) => typeof g === "string") : [],
3025
+ confidence: Math.min(1, Math.max(0, c.confidence))
3026
+ };
3027
+ }
3028
+ function toTitleCase(kebab) {
3029
+ return kebab.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3030
+ }
3031
+ function formatSkillMarkdown(candidate, postmortemFilename) {
3032
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3033
+ const heading = toTitleCase(candidate.name);
3034
+ const triggerStr = candidate.triggers.join(",");
3035
+ const lines = [
3036
+ `# ${heading}`,
3037
+ `<!-- aman-auto source=postmortem date=${date} confidence=${candidate.confidence} triggers="${triggerStr}" -->`,
3038
+ "",
3039
+ "## When to use",
3040
+ candidate.approach,
3041
+ "",
3042
+ "## Steps",
3043
+ ...candidate.steps.map((s, i) => `${i + 1}. ${s}`),
3044
+ ""
3045
+ ];
3046
+ if (candidate.gotchas.length > 0) {
3047
+ lines.push("## Gotchas");
3048
+ lines.push(...candidate.gotchas.map((g) => `- ${g}`));
3049
+ lines.push("");
3050
+ }
3051
+ lines.push(`<!-- generated from ${postmortemFilename} -->`);
3052
+ lines.push("");
3053
+ return lines.join("\n");
3054
+ }
3055
+ function parseMarkerComment(line) {
3056
+ const match = line.match(MARKER_RE);
3057
+ if (!match) return null;
3058
+ const attrs = {};
3059
+ const attrRe = /(\w+)=(?:"([^"]*)"|(\S+))/g;
3060
+ let m;
3061
+ while ((m = attrRe.exec(match[1])) !== null) {
3062
+ attrs[m[1]] = m[2] ?? m[3] ?? "";
3063
+ }
3064
+ if (!attrs.triggers) return null;
3065
+ const triggers = attrs.triggers.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
3066
+ if (triggers.length === 0) return null;
3067
+ return {
3068
+ source: attrs.source ?? "unknown",
3069
+ date: attrs.date ?? "",
3070
+ confidence: attrs.confidence ? Number(attrs.confidence) : 0,
3071
+ triggers
3072
+ };
3073
+ }
3074
+ function extractSkillsWithMarkers(skillsMdContent) {
3075
+ const result = /* @__PURE__ */ new Map();
3076
+ const lines = skillsMdContent.split("\n");
3077
+ for (let i = 0; i < lines.length; i++) {
3078
+ const line = lines[i];
3079
+ if (line.startsWith("# ") && i + 1 < lines.length) {
3080
+ const headingText = line.slice(2).trim();
3081
+ const nextLine = lines[i + 1];
3082
+ const marker = parseMarkerComment(nextLine);
3083
+ if (marker) {
3084
+ try {
3085
+ const skillName = sanitizeName(headingText);
3086
+ result.set(skillName, marker);
3087
+ } catch {
3088
+ log.debug("crystallization", `cannot sanitize heading: ${headingText}`);
3089
+ }
3090
+ }
3091
+ }
3092
+ }
3093
+ return result;
3094
+ }
3095
+ function findCollision(name, triggers, existing) {
3096
+ if (existing.has(name)) {
3097
+ return { collides: true, collidesWith: name, reason: "exact name match" };
3098
+ }
3099
+ const triggerSet = new Set(triggers);
3100
+ for (const [otherName, otherData] of existing) {
3101
+ const otherTriggers = new Set(otherData.triggers);
3102
+ const intersection = [...triggerSet].filter((t) => otherTriggers.has(t)).length;
3103
+ const union = (/* @__PURE__ */ new Set([...triggerSet, ...otherTriggers])).size;
3104
+ const overlap = union > 0 ? intersection / union : 0;
3105
+ if (overlap >= 0.8) {
3106
+ return {
3107
+ collides: true,
3108
+ collidesWith: otherName,
3109
+ reason: `${Math.round(overlap * 100)}% trigger overlap`
3110
+ };
3111
+ }
3112
+ }
3113
+ return { collides: false };
3114
+ }
3115
+ async function writeSkillToFile(candidate, skillsMdPath, postmortemFilename) {
3116
+ try {
3117
+ await fs12.mkdir(path12.dirname(skillsMdPath), { recursive: true });
3118
+ let existingContent = "";
3119
+ try {
3120
+ existingContent = await fs12.readFile(skillsMdPath, "utf-8");
3121
+ } catch {
3122
+ existingContent = "# Skills\n\n";
3123
+ }
3124
+ if (existingContent.trim() === "") {
3125
+ existingContent = "# Skills\n\n";
3126
+ }
3127
+ const existingSkills = extractSkillsWithMarkers(existingContent);
3128
+ const collision = findCollision(candidate.name, candidate.triggers, existingSkills);
3129
+ if (collision.collides) {
3130
+ log.debug("crystallization", `collision detected: ${collision.reason}`);
3131
+ return {
3132
+ written: false,
3133
+ filePath: skillsMdPath,
3134
+ skillName: candidate.name,
3135
+ reason: `collision with "${collision.collidesWith}" (${collision.reason})`
3136
+ };
3137
+ }
3138
+ const skillMarkdown = formatSkillMarkdown(candidate, postmortemFilename);
3139
+ const separator = existingContent.endsWith("\n\n") ? "" : existingContent.endsWith("\n") ? "\n" : "\n\n";
3140
+ await fs12.writeFile(
3141
+ skillsMdPath,
3142
+ existingContent + separator + skillMarkdown,
3143
+ "utf-8"
3144
+ );
3145
+ return {
3146
+ written: true,
3147
+ filePath: skillsMdPath,
3148
+ skillName: candidate.name
3149
+ };
3150
+ } catch (err) {
3151
+ log.warn("crystallization", "writeSkillToFile failed", err);
3152
+ return {
3153
+ written: false,
3154
+ filePath: skillsMdPath,
3155
+ skillName: candidate.name,
3156
+ reason: err instanceof Error ? err.message : String(err)
3157
+ };
3158
+ }
3159
+ }
3160
+ async function appendCrystallizationLog(entry, logPath) {
3161
+ try {
3162
+ await fs12.mkdir(path12.dirname(logPath), { recursive: true });
3163
+ let existing = [];
3164
+ try {
3165
+ const content = await fs12.readFile(logPath, "utf-8");
3166
+ existing = JSON.parse(content);
3167
+ if (!Array.isArray(existing)) existing = [];
3168
+ } catch {
3169
+ existing = [];
3170
+ }
3171
+ existing.push(entry);
3172
+ await fs12.writeFile(logPath, JSON.stringify(existing, null, 2), "utf-8");
3173
+ } catch (err) {
3174
+ log.debug("crystallization", "appendCrystallizationLog failed", err);
3175
+ }
3176
+ }
3177
+ async function appendRejection(candidate, postmortemFilename, rejectionsPath) {
3178
+ try {
3179
+ await fs12.mkdir(path12.dirname(rejectionsPath), { recursive: true });
3180
+ let existing = [];
3181
+ try {
3182
+ const content = await fs12.readFile(rejectionsPath, "utf-8");
3183
+ existing = JSON.parse(content);
3184
+ if (!Array.isArray(existing)) existing = [];
3185
+ } catch {
3186
+ existing = [];
3187
+ }
3188
+ existing.push({
3189
+ name: candidate.name,
3190
+ rejectedAt: (/* @__PURE__ */ new Date()).toISOString(),
3191
+ fromPostmortem: postmortemFilename,
3192
+ triggers: candidate.triggers
3193
+ });
3194
+ while (existing.length > MAX_REJECTIONS) {
3195
+ existing.shift();
3196
+ }
3197
+ await fs12.writeFile(rejectionsPath, JSON.stringify(existing, null, 2), "utf-8");
3198
+ } catch (err) {
3199
+ log.debug("crystallization", "appendRejection failed", err);
3200
+ }
3201
+ }
3202
+
2914
3203
  // src/hooks.ts
2915
3204
  function getTimeContext() {
2916
3205
  const now = /* @__PURE__ */ new Date();
@@ -3179,10 +3468,10 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3179
3468
  }
3180
3469
  console.log(pc2.dim(` Saved ${textMessages.length} messages (session: ${sessionId})`));
3181
3470
  }
3182
- const projectContextPath = path12.join(process.cwd(), ".acore", "context.md");
3183
- if (fs12.existsSync(projectContextPath) && messages.length > 2) {
3471
+ const projectContextPath = path13.join(process.cwd(), ".acore", "context.md");
3472
+ if (fs13.existsSync(projectContextPath) && messages.length > 2) {
3184
3473
  try {
3185
- let contextContent = fs12.readFileSync(projectContextPath, "utf-8");
3474
+ let contextContent = fs13.readFileSync(projectContextPath, "utf-8");
3186
3475
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3187
3476
  let lastUserMsg = "";
3188
3477
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -3200,7 +3489,7 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3200
3489
  - Recent decisions: [see memory]
3201
3490
  - Temp notes: [cleared]`;
3202
3491
  contextContent = contextContent.replace(sessionPattern, newSession);
3203
- fs12.writeFileSync(projectContextPath, contextContent, "utf-8");
3492
+ fs13.writeFileSync(projectContextPath, contextContent, "utf-8");
3204
3493
  log.debug("hooks", `Updated project context: ${projectContextPath}`);
3205
3494
  }
3206
3495
  } catch (err) {
@@ -3278,6 +3567,75 @@ async function onSessionEnd(ctx, messages, sessionId, observationSession) {
3278
3567
  } catch {
3279
3568
  }
3280
3569
  }
3570
+ if (report.crystallizationCandidates && report.crystallizationCandidates.length > 0) {
3571
+ const skillsMdPath = path13.join(os12.homedir(), ".askill", "skills.md");
3572
+ const logPath = path13.join(
3573
+ os12.homedir(),
3574
+ ".aman-agent",
3575
+ "crystallization-log.json"
3576
+ );
3577
+ const rejectionsPath = path13.join(
3578
+ os12.homedir(),
3579
+ ".aman-agent",
3580
+ "crystallization-rejections.json"
3581
+ );
3582
+ const postmortemFilename = `${report.date}-${report.sessionId.slice(0, 4)}.md`;
3583
+ console.log(
3584
+ pc2.dim(`
3585
+ Crystallization candidates: ${report.crystallizationCandidates.length}`)
3586
+ );
3587
+ let skipAll = false;
3588
+ for (const rawCandidate of report.crystallizationCandidates) {
3589
+ if (skipAll) break;
3590
+ const candidate = validateCandidate(rawCandidate);
3591
+ if (!candidate) {
3592
+ log.debug("hooks", "candidate failed validation");
3593
+ continue;
3594
+ }
3595
+ const choice = await p2.select({
3596
+ message: `Crystallize "${candidate.name}" as a reusable skill?`,
3597
+ options: [
3598
+ { value: "accept", label: "Yes \u2014 write to ~/.askill/skills.md" },
3599
+ { value: "reject", label: "No \u2014 skip this one" },
3600
+ { value: "skip-all", label: "Skip all crystallization for this session" }
3601
+ ],
3602
+ initialValue: "reject"
3603
+ });
3604
+ if (p2.isCancel(choice) || choice === "skip-all") {
3605
+ skipAll = true;
3606
+ break;
3607
+ }
3608
+ if (choice === "accept") {
3609
+ const result = await writeSkillToFile(
3610
+ candidate,
3611
+ skillsMdPath,
3612
+ postmortemFilename
3613
+ );
3614
+ if (result.written) {
3615
+ console.log(
3616
+ pc2.green(` \u2713 Crystallized: ${candidate.name} \u2192 ${result.filePath}`)
3617
+ );
3618
+ console.log(pc2.dim(` Triggers: ${candidate.triggers.join(", ")}`));
3619
+ console.log(pc2.dim(` Will auto-activate next session.`));
3620
+ await appendCrystallizationLog(
3621
+ {
3622
+ name: candidate.name,
3623
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3624
+ fromPostmortem: postmortemFilename,
3625
+ confidence: candidate.confidence,
3626
+ triggers: candidate.triggers
3627
+ },
3628
+ logPath
3629
+ );
3630
+ } else {
3631
+ console.log(pc2.yellow(` \u2298 Could not crystallize: ${result.reason}`));
3632
+ }
3633
+ } else {
3634
+ console.log(pc2.dim(` Skipped: ${candidate.name}`));
3635
+ await appendRejection(candidate, postmortemFilename, rejectionsPath);
3636
+ }
3637
+ }
3638
+ }
3281
3639
  }
3282
3640
  }
3283
3641
  } catch (err) {
@@ -3437,43 +3795,43 @@ async function delegatePipeline(steps, initialInput, client, mcpManager, options
3437
3795
  }
3438
3796
 
3439
3797
  // src/teams.ts
3440
- import fs13 from "fs";
3441
- import path13 from "path";
3442
- import os12 from "os";
3798
+ import fs14 from "fs";
3799
+ import path14 from "path";
3800
+ import os13 from "os";
3443
3801
  import pc4 from "picocolors";
3444
3802
  function getTeamsDir() {
3445
- return path13.join(os12.homedir(), ".acore", "teams");
3803
+ return path14.join(os13.homedir(), ".acore", "teams");
3446
3804
  }
3447
3805
  function ensureTeamsDir() {
3448
3806
  const dir = getTeamsDir();
3449
- if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
3807
+ if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
3450
3808
  return dir;
3451
3809
  }
3452
3810
  function teamPath(name) {
3453
3811
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
3454
- return path13.join(ensureTeamsDir(), `${slug}.json`);
3812
+ return path14.join(ensureTeamsDir(), `${slug}.json`);
3455
3813
  }
3456
3814
  function createTeam(team) {
3457
3815
  const fp = teamPath(team.name);
3458
- fs13.writeFileSync(fp, JSON.stringify(team, null, 2), "utf-8");
3816
+ fs14.writeFileSync(fp, JSON.stringify(team, null, 2), "utf-8");
3459
3817
  }
3460
3818
  function loadTeam(name) {
3461
3819
  const fp = teamPath(name);
3462
- if (!fs13.existsSync(fp)) return null;
3820
+ if (!fs14.existsSync(fp)) return null;
3463
3821
  try {
3464
- return JSON.parse(fs13.readFileSync(fp, "utf-8"));
3822
+ return JSON.parse(fs14.readFileSync(fp, "utf-8"));
3465
3823
  } catch {
3466
3824
  return null;
3467
3825
  }
3468
3826
  }
3469
3827
  function listTeams() {
3470
3828
  const dir = getTeamsDir();
3471
- if (!fs13.existsSync(dir)) return [];
3829
+ if (!fs14.existsSync(dir)) return [];
3472
3830
  const teams = [];
3473
- for (const file of fs13.readdirSync(dir)) {
3831
+ for (const file of fs14.readdirSync(dir)) {
3474
3832
  if (!file.endsWith(".json")) continue;
3475
3833
  try {
3476
- const content = fs13.readFileSync(path13.join(dir, file), "utf-8");
3834
+ const content = fs14.readFileSync(path14.join(dir, file), "utf-8");
3477
3835
  teams.push(JSON.parse(content));
3478
3836
  } catch {
3479
3837
  }
@@ -3482,8 +3840,8 @@ function listTeams() {
3482
3840
  }
3483
3841
  function deleteTeam(name) {
3484
3842
  const fp = teamPath(name);
3485
- if (!fs13.existsSync(fp)) return false;
3486
- fs13.unlinkSync(fp);
3843
+ if (!fs14.existsSync(fp)) return false;
3844
+ fs14.unlinkSync(fp);
3487
3845
  return true;
3488
3846
  }
3489
3847
  async function runTeam(team, task, client, mcpManager, tools) {
@@ -3709,23 +4067,23 @@ var BUILT_IN_TEAMS = [
3709
4067
  ];
3710
4068
 
3711
4069
  // src/plans.ts
3712
- import fs14 from "fs";
3713
- import path14 from "path";
3714
- import os13 from "os";
4070
+ import fs15 from "fs";
4071
+ import path15 from "path";
4072
+ import os14 from "os";
3715
4073
  function getPlansDir() {
3716
- const localDir = path14.join(process.cwd(), ".acore", "plans");
3717
- const localAcore = path14.join(process.cwd(), ".acore");
3718
- if (fs14.existsSync(localAcore)) return localDir;
3719
- return path14.join(os13.homedir(), ".acore", "plans");
4074
+ const localDir = path15.join(process.cwd(), ".acore", "plans");
4075
+ const localAcore = path15.join(process.cwd(), ".acore");
4076
+ if (fs15.existsSync(localAcore)) return localDir;
4077
+ return path15.join(os14.homedir(), ".acore", "plans");
3720
4078
  }
3721
4079
  function ensurePlansDir() {
3722
4080
  const dir = getPlansDir();
3723
- if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
4081
+ if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
3724
4082
  return dir;
3725
4083
  }
3726
4084
  function planPath(name) {
3727
4085
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
3728
- return path14.join(ensurePlansDir(), `${slug}.md`);
4086
+ return path15.join(ensurePlansDir(), `${slug}.md`);
3729
4087
  }
3730
4088
  function serializePlan(plan) {
3731
4089
  const lines = [];
@@ -3751,7 +4109,7 @@ function parsePlan(content, filePath) {
3751
4109
  const createdMatch = content.match(/\*\*Created:\*\*\s*(.+)/);
3752
4110
  const updatedMatch = content.match(/\*\*Updated:\*\*\s*(.+)/);
3753
4111
  const activeMatch = content.match(/\*\*Active:\*\*\s*(.+)/);
3754
- const name = nameMatch?.[1]?.trim() || path14.basename(filePath, ".md");
4112
+ const name = nameMatch?.[1]?.trim() || path15.basename(filePath, ".md");
3755
4113
  const goal = goalMatch?.[1]?.trim() || "";
3756
4114
  const createdAt = createdMatch?.[1]?.trim() || "";
3757
4115
  const updatedAt = updatedMatch?.[1]?.trim() || "";
@@ -3793,22 +4151,22 @@ function createPlan(name, goal, steps) {
3793
4151
  }
3794
4152
  function savePlan(plan) {
3795
4153
  const fp = planPath(plan.name);
3796
- fs14.writeFileSync(fp, serializePlan(plan), "utf-8");
4154
+ fs15.writeFileSync(fp, serializePlan(plan), "utf-8");
3797
4155
  }
3798
4156
  function loadPlan(name) {
3799
4157
  const fp = planPath(name);
3800
- if (!fs14.existsSync(fp)) return null;
3801
- const content = fs14.readFileSync(fp, "utf-8");
4158
+ if (!fs15.existsSync(fp)) return null;
4159
+ const content = fs15.readFileSync(fp, "utf-8");
3802
4160
  return parsePlan(content, fp);
3803
4161
  }
3804
4162
  function listPlans() {
3805
4163
  const dir = getPlansDir();
3806
- if (!fs14.existsSync(dir)) return [];
4164
+ if (!fs15.existsSync(dir)) return [];
3807
4165
  const plans = [];
3808
- for (const file of fs14.readdirSync(dir)) {
4166
+ for (const file of fs15.readdirSync(dir)) {
3809
4167
  if (!file.endsWith(".md")) continue;
3810
- const fp = path14.join(dir, file);
3811
- const content = fs14.readFileSync(fp, "utf-8");
4168
+ const fp = path15.join(dir, file);
4169
+ const content = fs15.readFileSync(fp, "utf-8");
3812
4170
  const plan = parsePlan(content, fp);
3813
4171
  if (plan) plans.push(plan);
3814
4172
  }
@@ -3917,10 +4275,10 @@ import {
3917
4275
  } from "@aman_asmuei/arules-core";
3918
4276
  var AGENT_SCOPE = process.env.AMAN_AGENT_SCOPE ?? "dev:agent";
3919
4277
  function readEcosystemFile(filePath, label) {
3920
- if (!fs15.existsSync(filePath)) {
4278
+ if (!fs16.existsSync(filePath)) {
3921
4279
  return pc5.dim(`No ${label} file found at ${filePath}`);
3922
4280
  }
3923
- return fs15.readFileSync(filePath, "utf-8").trim();
4281
+ return fs16.readFileSync(filePath, "utf-8").trim();
3924
4282
  }
3925
4283
  function parseCommand(input) {
3926
4284
  const trimmed = input.trim();
@@ -4195,9 +4553,9 @@ ${result.violations.map((v) => ` - ${v}`).join("\n")}`)
4195
4553
  };
4196
4554
  }
4197
4555
  async function handleWorkflowsCommand(action, args, ctx) {
4198
- const home2 = os14.homedir();
4556
+ const home2 = os15.homedir();
4199
4557
  if (!action) {
4200
- const content = readEcosystemFile(path15.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
4558
+ const content = readEcosystemFile(path16.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
4201
4559
  return { handled: true, output: content };
4202
4560
  }
4203
4561
  if (action === "add") {
@@ -4219,7 +4577,7 @@ async function handleWorkflowsCommand(action, args, ctx) {
4219
4577
  return { handled: true, output: pc5.yellow("Usage: /workflows get <name>") };
4220
4578
  }
4221
4579
  const name = args.join(" ").toLowerCase();
4222
- const raw = readEcosystemFile(path15.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
4580
+ const raw = readEcosystemFile(path16.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
4223
4581
  if (raw.startsWith("No ")) {
4224
4582
  return { handled: true, output: raw };
4225
4583
  }
@@ -4278,12 +4636,12 @@ async function handleToolsCommand(action, args, _ctx) {
4278
4636
  return { handled: true, output: pc5.yellow("Usage: /tools search <query...>") };
4279
4637
  }
4280
4638
  const query = args.join(" ").toLowerCase();
4281
- const home2 = os14.homedir();
4282
- const toolsFile = path15.join(home2, ".akit", "tools.md");
4283
- if (!fs15.existsSync(toolsFile)) {
4639
+ const home2 = os15.homedir();
4640
+ const toolsFile = path16.join(home2, ".akit", "tools.md");
4641
+ if (!fs16.existsSync(toolsFile)) {
4284
4642
  return { handled: true, output: pc5.dim(`No tools file found. Use 'npx @aman_asmuei/akit search ${args.join(" ")}' to search the registry.`) };
4285
4643
  }
4286
- const raw = fs15.readFileSync(toolsFile, "utf-8").trim();
4644
+ const raw = fs16.readFileSync(toolsFile, "utf-8").trim();
4287
4645
  const lines = raw.split("\n");
4288
4646
  const matches = lines.filter((l) => l.toLowerCase().includes(query));
4289
4647
  if (matches.length === 0) {
@@ -4294,9 +4652,9 @@ async function handleToolsCommand(action, args, _ctx) {
4294
4652
  return handleAkitCommand(action, args);
4295
4653
  }
4296
4654
  async function handleSkillsCommand(action, args, ctx) {
4297
- const home2 = os14.homedir();
4655
+ const home2 = os15.homedir();
4298
4656
  if (!action) {
4299
- const content = readEcosystemFile(path15.join(home2, ".askill", "skills.md"), "skills (askill)");
4657
+ const content = readEcosystemFile(path16.join(home2, ".askill", "skills.md"), "skills (askill)");
4300
4658
  return { handled: true, output: content };
4301
4659
  }
4302
4660
  if (action === "install") {
@@ -4318,8 +4676,8 @@ async function handleSkillsCommand(action, args, ctx) {
4318
4676
  return { handled: true, output: pc5.yellow("Usage: /skills search <query...>") };
4319
4677
  }
4320
4678
  const query = args.join(" ").toLowerCase();
4321
- const home3 = os14.homedir();
4322
- const raw = readEcosystemFile(path15.join(home3, ".askill", "skills.md"), "skills (askill)");
4679
+ const home3 = os15.homedir();
4680
+ const raw = readEcosystemFile(path16.join(home3, ".askill", "skills.md"), "skills (askill)");
4323
4681
  if (raw.startsWith("No ")) {
4324
4682
  return { handled: true, output: raw };
4325
4683
  }
@@ -4330,21 +4688,111 @@ async function handleSkillsCommand(action, args, ctx) {
4330
4688
  }
4331
4689
  return { handled: true, output: [pc5.bold(`Skills matching "${query}":`), ...matches].join("\n") };
4332
4690
  }
4691
+ if (action === "list") {
4692
+ const autoOnly = args.includes("--auto");
4693
+ if (autoOnly) {
4694
+ const logPath = path16.join(os15.homedir(), ".aman-agent", "crystallization-log.json");
4695
+ try {
4696
+ const content2 = fs16.readFileSync(logPath, "utf-8");
4697
+ const entries = JSON.parse(content2);
4698
+ if (entries.length === 0) {
4699
+ return { handled: true, output: pc5.dim("No crystallized skills yet.") };
4700
+ }
4701
+ const lines = [pc5.bold(`Crystallized skills (${entries.length}):`)];
4702
+ for (const entry of entries) {
4703
+ const date = entry.createdAt.slice(0, 10);
4704
+ lines.push(` ${pc5.cyan(entry.name)} (${date}, conf ${entry.confidence})`);
4705
+ lines.push(pc5.dim(` triggers: ${entry.triggers.join(", ")}`));
4706
+ }
4707
+ return { handled: true, output: lines.join("\n") };
4708
+ } catch {
4709
+ return { handled: true, output: pc5.dim("No crystallized skills yet.") };
4710
+ }
4711
+ }
4712
+ const content = readEcosystemFile(path16.join(home2, ".askill", "skills.md"), "skills (askill)");
4713
+ return { handled: true, output: content };
4714
+ }
4715
+ if (action === "crystallize") {
4716
+ const pmDir = path16.join(os15.homedir(), ".acore", "postmortems");
4717
+ try {
4718
+ const files = fs16.readdirSync(pmDir);
4719
+ const jsonFiles = files.filter((f) => f.endsWith(".json")).sort().reverse();
4720
+ if (jsonFiles.length === 0) {
4721
+ return {
4722
+ handled: true,
4723
+ output: pc5.dim("No post-mortems found. Run a session that triggers a post-mortem first.")
4724
+ };
4725
+ }
4726
+ const latest = jsonFiles[0];
4727
+ const content = fs16.readFileSync(path16.join(pmDir, latest), "utf-8");
4728
+ const report = JSON.parse(content);
4729
+ if (!report.crystallizationCandidates || report.crystallizationCandidates.length === 0) {
4730
+ return {
4731
+ handled: true,
4732
+ output: pc5.dim(`No crystallization candidates in the most recent post-mortem (${latest}). Run a longer session or wait for the next auto-postmortem.`)
4733
+ };
4734
+ }
4735
+ const skillsMdPath = path16.join(os15.homedir(), ".askill", "skills.md");
4736
+ const logPath = path16.join(os15.homedir(), ".aman-agent", "crystallization-log.json");
4737
+ const postmortemFilename = latest.replace(/\.json$/, ".md");
4738
+ const lines = [
4739
+ pc5.bold(`Found ${report.crystallizationCandidates.length} candidate(s) in ${latest}:`)
4740
+ ];
4741
+ let written = 0;
4742
+ for (const raw of report.crystallizationCandidates) {
4743
+ const candidate = validateCandidate(raw);
4744
+ if (!candidate) {
4745
+ const rawName = raw.name ?? "unknown";
4746
+ lines.push(pc5.dim(` \u2298 ${rawName} \u2014 failed validation`));
4747
+ continue;
4748
+ }
4749
+ const result = await writeSkillToFile(candidate, skillsMdPath, postmortemFilename);
4750
+ if (result.written) {
4751
+ written++;
4752
+ lines.push(pc5.green(` \u2713 Crystallized: ${candidate.name}`));
4753
+ await appendCrystallizationLog(
4754
+ {
4755
+ name: candidate.name,
4756
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4757
+ fromPostmortem: postmortemFilename,
4758
+ confidence: candidate.confidence,
4759
+ triggers: candidate.triggers
4760
+ },
4761
+ logPath
4762
+ );
4763
+ } else {
4764
+ lines.push(pc5.yellow(` \u2298 ${candidate.name} \u2014 ${result.reason}`));
4765
+ }
4766
+ }
4767
+ if (written > 0) {
4768
+ lines.push("");
4769
+ lines.push(pc5.dim(`Crystallized skills will auto-activate in your next session.`));
4770
+ }
4771
+ return { handled: true, output: lines.join("\n") };
4772
+ } catch (err) {
4773
+ return {
4774
+ handled: true,
4775
+ output: pc5.red(`Failed to load post-mortems: ${err instanceof Error ? err.message : String(err)}`)
4776
+ };
4777
+ }
4778
+ }
4333
4779
  if (action === "help") {
4334
4780
  return { handled: true, output: [
4335
4781
  pc5.bold("Skills commands:"),
4336
- ` ${pc5.cyan("/skills")} View installed skills`,
4337
- ` ${pc5.cyan("/skills install")} <name> Install a skill`,
4338
- ` ${pc5.cyan("/skills uninstall")} <name> Uninstall a skill`,
4339
- ` ${pc5.cyan("/skills search")} <query> Search skills by name/description`
4782
+ ` ${pc5.cyan("/skills")} View installed skills`,
4783
+ ` ${pc5.cyan("/skills install")} <name> Install a skill`,
4784
+ ` ${pc5.cyan("/skills uninstall")} <name> Uninstall a skill`,
4785
+ ` ${pc5.cyan("/skills search")} <query> Search skills by name/description`,
4786
+ ` ${pc5.cyan("/skills crystallize")} Crystallize skills from most recent post-mortem`,
4787
+ ` ${pc5.cyan("/skills list --auto")} List crystallized (auto-created) skills`
4340
4788
  ].join("\n") };
4341
4789
  }
4342
4790
  return { handled: true, output: pc5.yellow(`Unknown action: /skills ${action}. Try /skills --help`) };
4343
4791
  }
4344
4792
  async function handleEvalCommand(action, args, ctx) {
4345
- const home2 = os14.homedir();
4793
+ const home2 = os15.homedir();
4346
4794
  if (!action) {
4347
- const content = readEcosystemFile(path15.join(home2, ".aeval", "eval.md"), "evaluation (aeval)");
4795
+ const content = readEcosystemFile(path16.join(home2, ".aeval", "eval.md"), "evaluation (aeval)");
4348
4796
  return { handled: true, output: content };
4349
4797
  }
4350
4798
  if (action === "milestone") {
@@ -4356,11 +4804,11 @@ async function handleEvalCommand(action, args, ctx) {
4356
4804
  return { handled: true, output };
4357
4805
  }
4358
4806
  if (action === "report") {
4359
- const evalFile = path15.join(home2, ".aeval", "eval.md");
4360
- if (!fs15.existsSync(evalFile)) {
4807
+ const evalFile = path16.join(home2, ".aeval", "eval.md");
4808
+ if (!fs16.existsSync(evalFile)) {
4361
4809
  return { handled: true, output: pc5.dim("No eval report found. Log milestones with /eval milestone <text>.") };
4362
4810
  }
4363
- const content = fs15.readFileSync(evalFile, "utf-8").trim();
4811
+ const content = fs16.readFileSync(evalFile, "utf-8").trim();
4364
4812
  return { handled: true, output: [pc5.bold("Eval Report"), "", content].join("\n") };
4365
4813
  }
4366
4814
  return { handled: true, output: pc5.yellow(`Unknown action: /eval ${action}. Use /eval, /eval report, or /eval milestone <text>.`) };
@@ -4852,7 +5300,7 @@ function handleHelp() {
4852
5300
  ` ${pc5.cyan("/rules")} View rules [add|remove|toggle ...]`,
4853
5301
  ` ${pc5.cyan("/workflows")} View workflows [add|remove ...]`,
4854
5302
  ` ${pc5.cyan("/akit")} Manage tools [add|remove <tool>]`,
4855
- ` ${pc5.cyan("/skills")} View skills [install|uninstall ...]`,
5303
+ ` ${pc5.cyan("/skills")} View skills [install|uninstall|crystallize|list --auto]`,
4856
5304
  ` ${pc5.cyan("/eval")} View evaluation [milestone ...]`,
4857
5305
  ` ${pc5.cyan("/memory")} View recent memories [search|fts|since|stats|export|clear|timeline]`,
4858
5306
  ` ${pc5.cyan("/reminder")} Manage reminders [set|check|done]`,
@@ -4884,10 +5332,10 @@ function handleSave() {
4884
5332
  }
4885
5333
  function handleReset(action) {
4886
5334
  const dirs = {
4887
- config: path15.join(os14.homedir(), ".aman-agent"),
4888
- memory: path15.join(os14.homedir(), ".amem"),
4889
- identity: path15.join(os14.homedir(), ".acore"),
4890
- rules: path15.join(os14.homedir(), ".arules")
5335
+ config: path16.join(os15.homedir(), ".aman-agent"),
5336
+ memory: path16.join(os15.homedir(), ".amem"),
5337
+ identity: path16.join(os15.homedir(), ".acore"),
5338
+ rules: path16.join(os15.homedir(), ".arules")
4891
5339
  };
4892
5340
  if (action === "help" || !action) {
4893
5341
  return {
@@ -4912,15 +5360,15 @@ function handleReset(action) {
4912
5360
  const removed = [];
4913
5361
  for (const target of targets) {
4914
5362
  const dir = dirs[target];
4915
- if (fs15.existsSync(dir)) {
4916
- fs15.rmSync(dir, { recursive: true, force: true });
5363
+ if (fs16.existsSync(dir)) {
5364
+ fs16.rmSync(dir, { recursive: true, force: true });
4917
5365
  removed.push(target);
4918
5366
  }
4919
5367
  }
4920
5368
  if (targets.includes("config")) {
4921
5369
  const configDir = dirs.config;
4922
- fs15.mkdirSync(configDir, { recursive: true });
4923
- fs15.writeFileSync(path15.join(configDir, ".reconfig"), "", "utf-8");
5370
+ fs16.mkdirSync(configDir, { recursive: true });
5371
+ fs16.writeFileSync(path16.join(configDir, ".reconfig"), "", "utf-8");
4924
5372
  }
4925
5373
  if (removed.length === 0) {
4926
5374
  return { handled: true, output: pc5.dim("Nothing to reset \u2014 directories don't exist.") };
@@ -4937,7 +5385,7 @@ function handleReset(action) {
4937
5385
  function handleUpdate() {
4938
5386
  try {
4939
5387
  const current = execFileSync3("npm", ["view", "@aman_asmuei/aman-agent", "version"], { encoding: "utf-8" }).trim();
4940
- const local = true ? "0.25.0" : "unknown";
5388
+ const local = true ? "0.26.0" : "unknown";
4941
5389
  if (current === local) {
4942
5390
  return { handled: true, output: `${pc5.green("Up to date")} \u2014 v${local}` };
4943
5391
  }
@@ -4981,11 +5429,11 @@ function handleExportCommand() {
4981
5429
  return { handled: true, exportConversation: true };
4982
5430
  }
4983
5431
  function handleDebugCommand() {
4984
- const logPath = path15.join(os14.homedir(), ".aman-agent", "debug.log");
4985
- if (!fs15.existsSync(logPath)) {
5432
+ const logPath = path16.join(os15.homedir(), ".aman-agent", "debug.log");
5433
+ if (!fs16.existsSync(logPath)) {
4986
5434
  return { handled: true, output: pc5.dim("No debug log found.") };
4987
5435
  }
4988
- const content = fs15.readFileSync(logPath, "utf-8");
5436
+ const content = fs16.readFileSync(logPath, "utf-8");
4989
5437
  const lines = content.trim().split("\n");
4990
5438
  const last20 = lines.slice(-20).join("\n");
4991
5439
  return { handled: true, output: pc5.bold("Debug Log (last 20 entries):\n") + pc5.dim(last20) };
@@ -5183,7 +5631,7 @@ ${result.response}`
5183
5631
  };
5184
5632
  }
5185
5633
  function handleProfileCommand(action, args) {
5186
- const profilesDir = path15.join(os14.homedir(), ".acore", "profiles");
5634
+ const profilesDir = path16.join(os15.homedir(), ".acore", "profiles");
5187
5635
  if (action === "me") {
5188
5636
  const user = loadUserIdentity();
5189
5637
  if (!user) {
@@ -5251,8 +5699,8 @@ ${pc5.dim("Edit with: /profile edit")}` };
5251
5699
  };
5252
5700
  }
5253
5701
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
5254
- const profileDir = path15.join(profilesDir, slug);
5255
- if (fs15.existsSync(profileDir)) {
5702
+ const profileDir = path16.join(profilesDir, slug);
5703
+ if (fs16.existsSync(profileDir)) {
5256
5704
  return { handled: true, output: pc5.yellow(`Profile already exists: ${slug}`) };
5257
5705
  }
5258
5706
  const builtIn = BUILT_IN_PROFILES.find((t) => t.name === slug);
@@ -5268,16 +5716,16 @@ ${pc5.dim("Edit with: /profile edit")}` };
5268
5716
  Use: aman-agent --profile ${slug}`
5269
5717
  };
5270
5718
  }
5271
- fs15.mkdirSync(profileDir, { recursive: true });
5272
- const globalCore = path15.join(os14.homedir(), ".acore", "core.md");
5273
- if (fs15.existsSync(globalCore)) {
5274
- let content = fs15.readFileSync(globalCore, "utf-8");
5719
+ fs16.mkdirSync(profileDir, { recursive: true });
5720
+ const globalCore = path16.join(os15.homedir(), ".acore", "core.md");
5721
+ if (fs16.existsSync(globalCore)) {
5722
+ let content = fs16.readFileSync(globalCore, "utf-8");
5275
5723
  const aiName = name.charAt(0).toUpperCase() + name.slice(1);
5276
5724
  content = content.replace(/^# .+$/m, `# ${aiName}`);
5277
- fs15.writeFileSync(path15.join(profileDir, "core.md"), content, "utf-8");
5725
+ fs16.writeFileSync(path16.join(profileDir, "core.md"), content, "utf-8");
5278
5726
  } else {
5279
5727
  const aiName = name.charAt(0).toUpperCase() + name.slice(1);
5280
- fs15.writeFileSync(path15.join(profileDir, "core.md"), `# ${aiName}
5728
+ fs16.writeFileSync(path16.join(profileDir, "core.md"), `# ${aiName}
5281
5729
 
5282
5730
  ## Identity
5283
5731
  - Role: ${aiName} is your AI companion
@@ -5290,7 +5738,7 @@ ${pc5.dim("Edit with: /profile edit")}` };
5290
5738
  return {
5291
5739
  handled: true,
5292
5740
  output: pc5.green(`Profile created: ${slug}`) + `
5293
- Edit: ${path15.join(profileDir, "core.md")}
5741
+ Edit: ${path16.join(profileDir, "core.md")}
5294
5742
  Use: aman-agent --profile ${slug}
5295
5743
 
5296
5744
  ${pc5.dim("Add rules.md or skills.md for profile-specific overrides.")}`
@@ -5299,9 +5747,9 @@ ${pc5.dim("Edit with: /profile edit")}` };
5299
5747
  case "show": {
5300
5748
  const name = args[0];
5301
5749
  if (!name) return { handled: true, output: pc5.yellow("Usage: /profile show <name>") };
5302
- const profileDir = path15.join(profilesDir, name);
5303
- if (!fs15.existsSync(profileDir)) return { handled: true, output: pc5.red(`Profile not found: ${name}`) };
5304
- const files = fs15.readdirSync(profileDir).filter((f) => f.endsWith(".md"));
5750
+ const profileDir = path16.join(profilesDir, name);
5751
+ if (!fs16.existsSync(profileDir)) return { handled: true, output: pc5.red(`Profile not found: ${name}`) };
5752
+ const files = fs16.readdirSync(profileDir).filter((f) => f.endsWith(".md"));
5305
5753
  const lines = files.map((f) => ` ${f}`);
5306
5754
  return { handled: true, output: `Profile: ${pc5.bold(name)}
5307
5755
  Files:
@@ -5310,9 +5758,9 @@ ${lines.join("\n")}` };
5310
5758
  case "delete": {
5311
5759
  const name = args[0];
5312
5760
  if (!name) return { handled: true, output: pc5.yellow("Usage: /profile delete <name>") };
5313
- const profileDir = path15.join(profilesDir, name);
5314
- if (!fs15.existsSync(profileDir)) return { handled: true, output: pc5.red(`Profile not found: ${name}`) };
5315
- fs15.rmSync(profileDir, { recursive: true });
5761
+ const profileDir = path16.join(profilesDir, name);
5762
+ if (!fs16.existsSync(profileDir)) return { handled: true, output: pc5.red(`Profile not found: ${name}`) };
5763
+ fs16.rmSync(profileDir, { recursive: true });
5316
5764
  return { handled: true, output: pc5.dim(`Profile deleted: ${name}`) };
5317
5765
  }
5318
5766
  case "help":
@@ -5530,10 +5978,10 @@ function handleShowcaseCommand(action, args) {
5530
5978
  Or place it as a sibling directory to aman-agent.`
5531
5979
  };
5532
5980
  }
5533
- const corePath = path15.join(os14.homedir(), ".acore", "core.md");
5981
+ const corePath = path16.join(os15.homedir(), ".acore", "core.md");
5534
5982
  let currentShowcase = null;
5535
- if (fs15.existsSync(corePath)) {
5536
- const content = fs15.readFileSync(corePath, "utf-8");
5983
+ if (fs16.existsSync(corePath)) {
5984
+ const content = fs16.readFileSync(corePath, "utf-8");
5537
5985
  const nameMatch = content.match(/^# (.+)/m);
5538
5986
  if (nameMatch) {
5539
5987
  const coreName = nameMatch[1].trim().toLowerCase();
@@ -5920,9 +6368,10 @@ ${summaryParts.slice(0, 20).join("\n")}
5920
6368
  }
5921
6369
 
5922
6370
  // src/skill-engine.ts
5923
- import fs16 from "fs";
5924
- import path16 from "path";
5925
- import os15 from "os";
6371
+ import fs17 from "fs";
6372
+ import fsp from "fs/promises";
6373
+ import path17 from "path";
6374
+ import os16 from "os";
5926
6375
  var SKILL_TRIGGERS = {
5927
6376
  testing: ["test", "spec", "coverage", "tdd", "jest", "vitest", "mocha", "assert", "mock", "stub", "fixture", "e2e", "integration test", "unit test"],
5928
6377
  "api-design": ["api", "endpoint", "rest", "graphql", "route", "controller", "middleware", "http", "request", "response", "status code", "pagination"],
@@ -5937,20 +6386,34 @@ var SKILL_TRIGGERS = {
5937
6386
  typescript: ["typescript", "type", "interface", "generic", "infer", "utility type", "zod", "discriminated union", "type guard", "as const"],
5938
6387
  accessibility: ["accessibility", "a11y", "aria", "screen reader", "wcag", "semantic html", "tab order", "focus", "contrast"]
5939
6388
  };
5940
- var LEVEL_FILE = path16.join(os15.homedir(), ".aman-agent", "skill-levels.json");
6389
+ async function loadRuntimeTriggers(skillsMdPath) {
6390
+ try {
6391
+ const content = await fsp.readFile(skillsMdPath, "utf-8");
6392
+ const skills = extractSkillsWithMarkers(content);
6393
+ const result = /* @__PURE__ */ new Map();
6394
+ for (const [name, marker] of skills) {
6395
+ result.set(name, marker.triggers);
6396
+ }
6397
+ return result;
6398
+ } catch (err) {
6399
+ log.debug("skill-engine", "loadRuntimeTriggers failed", err);
6400
+ return /* @__PURE__ */ new Map();
6401
+ }
6402
+ }
6403
+ var LEVEL_FILE = path17.join(os16.homedir(), ".aman-agent", "skill-levels.json");
5941
6404
  function loadSkillLevels() {
5942
6405
  try {
5943
- if (fs16.existsSync(LEVEL_FILE)) {
5944
- return JSON.parse(fs16.readFileSync(LEVEL_FILE, "utf-8"));
6406
+ if (fs17.existsSync(LEVEL_FILE)) {
6407
+ return JSON.parse(fs17.readFileSync(LEVEL_FILE, "utf-8"));
5945
6408
  }
5946
6409
  } catch {
5947
6410
  }
5948
6411
  return {};
5949
6412
  }
5950
6413
  function saveSkillLevels(levels) {
5951
- const dir = path16.dirname(LEVEL_FILE);
5952
- if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
5953
- fs16.writeFileSync(LEVEL_FILE, JSON.stringify(levels, null, 2), "utf-8");
6414
+ const dir = path17.dirname(LEVEL_FILE);
6415
+ if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
6416
+ fs17.writeFileSync(LEVEL_FILE, JSON.stringify(levels, null, 2), "utf-8");
5954
6417
  }
5955
6418
  function computeLevel(activations) {
5956
6419
  if (activations >= 50) return { level: 5, label: "Expert" };
@@ -5969,20 +6432,28 @@ function recordActivation(skillName) {
5969
6432
  saveSkillLevels(levels);
5970
6433
  return computeLevel(levels[skillName].activations);
5971
6434
  }
5972
- function matchSkills(userInput, installedSkillNames) {
6435
+ function matchSkills(userInput, installedSkillNames, runtimeTriggers = /* @__PURE__ */ new Map()) {
5973
6436
  const input = userInput.toLowerCase();
5974
- const matched = [];
6437
+ const matched = /* @__PURE__ */ new Set();
5975
6438
  for (const skillName of installedSkillNames) {
5976
6439
  const triggers = SKILL_TRIGGERS[skillName];
5977
6440
  if (!triggers) continue;
5978
6441
  for (const trigger of triggers) {
5979
6442
  if (input.includes(trigger)) {
5980
- matched.push(skillName);
6443
+ matched.add(skillName);
5981
6444
  break;
5982
6445
  }
5983
6446
  }
5984
6447
  }
5985
- return matched;
6448
+ for (const [skillName, triggers] of runtimeTriggers) {
6449
+ for (const trigger of triggers) {
6450
+ if (input.includes(trigger)) {
6451
+ matched.add(skillName);
6452
+ break;
6453
+ }
6454
+ }
6455
+ }
6456
+ return Array.from(matched);
5986
6457
  }
5987
6458
  function formatSkillContext(skillName, skillContent, level) {
5988
6459
  let depthHint;
@@ -6006,8 +6477,10 @@ async function autoTriggerSkills(userInput, mcpManager) {
6006
6477
  const result = await mcpManager.callTool("skill_list", {});
6007
6478
  const skills = JSON.parse(result);
6008
6479
  const installed = skills.filter((s) => s.installed).map((s) => s.name);
6009
- if (installed.length === 0) return "";
6010
- const matched = matchSkills(userInput, installed);
6480
+ const skillsMdPath = path17.join(os16.homedir(), ".askill", "skills.md");
6481
+ const runtimeTriggers = await loadRuntimeTriggers(skillsMdPath);
6482
+ if (installed.length === 0 && runtimeTriggers.size === 0) return "";
6483
+ const matched = matchSkills(userInput, installed, runtimeTriggers);
6011
6484
  if (matched.length === 0) return "";
6012
6485
  const blocks = [];
6013
6486
  for (const skillName of matched.slice(0, 2)) {
@@ -6563,9 +7036,9 @@ function humanizeError(message) {
6563
7036
  }
6564
7037
 
6565
7038
  // src/hints.ts
6566
- import fs17 from "fs";
6567
- import path17 from "path";
6568
- import os16 from "os";
7039
+ import fs18 from "fs";
7040
+ import path18 from "path";
7041
+ import os17 from "os";
6569
7042
  var HINTS = [
6570
7043
  {
6571
7044
  id: "eval",
@@ -6603,11 +7076,11 @@ function getHint(state, ctx) {
6603
7076
  }
6604
7077
  return null;
6605
7078
  }
6606
- var HINTS_FILE = path17.join(os16.homedir(), ".aman-agent", "hints-seen.json");
7079
+ var HINTS_FILE = path18.join(os17.homedir(), ".aman-agent", "hints-seen.json");
6607
7080
  function loadShownHints() {
6608
7081
  try {
6609
- if (fs17.existsSync(HINTS_FILE)) {
6610
- const data = JSON.parse(fs17.readFileSync(HINTS_FILE, "utf-8"));
7082
+ if (fs18.existsSync(HINTS_FILE)) {
7083
+ const data = JSON.parse(fs18.readFileSync(HINTS_FILE, "utf-8"));
6611
7084
  return new Set(Array.isArray(data) ? data : []);
6612
7085
  }
6613
7086
  } catch {
@@ -6616,9 +7089,9 @@ function loadShownHints() {
6616
7089
  }
6617
7090
  function saveShownHints(shown) {
6618
7091
  try {
6619
- const dir = path17.dirname(HINTS_FILE);
6620
- fs17.mkdirSync(dir, { recursive: true });
6621
- fs17.writeFileSync(HINTS_FILE, JSON.stringify([...shown]), "utf-8");
7092
+ const dir = path18.dirname(HINTS_FILE);
7093
+ fs18.mkdirSync(dir, { recursive: true });
7094
+ fs18.writeFileSync(HINTS_FILE, JSON.stringify([...shown]), "utf-8");
6622
7095
  } catch {
6623
7096
  }
6624
7097
  }
@@ -6885,9 +7358,9 @@ ${task.result}`
6885
7358
  }
6886
7359
  if (cmdResult.exportConversation) {
6887
7360
  try {
6888
- const exportDir = path18.join(os17.homedir(), ".aman-agent", "exports");
6889
- fs18.mkdirSync(exportDir, { recursive: true });
6890
- const exportPath = path18.join(exportDir, `${sessionId}.md`);
7361
+ const exportDir = path19.join(os18.homedir(), ".aman-agent", "exports");
7362
+ fs19.mkdirSync(exportDir, { recursive: true });
7363
+ const exportPath = path19.join(exportDir, `${sessionId}.md`);
6891
7364
  const lines = [
6892
7365
  `# Conversation \u2014 ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
6893
7366
  `**Model:** ${model}`,
@@ -6901,7 +7374,7 @@ ${task.result}`
6901
7374
  lines.push(`${label} ${msg.content}`, "");
6902
7375
  }
6903
7376
  }
6904
- fs18.writeFileSync(exportPath, lines.join("\n"), "utf-8");
7377
+ fs19.writeFileSync(exportPath, lines.join("\n"), "utf-8");
6905
7378
  console.log(pc7.green(`Exported to ${exportPath}`));
6906
7379
  } catch {
6907
7380
  console.log(pc7.red("Failed to export conversation."));
@@ -7027,25 +7500,25 @@ ${knowledgeItem.content}
7027
7500
  for (const match of filePathMatches) {
7028
7501
  let filePath = match[1];
7029
7502
  if (filePath.startsWith("~/")) {
7030
- filePath = path18.join(os17.homedir(), filePath.slice(2));
7503
+ filePath = path19.join(os18.homedir(), filePath.slice(2));
7031
7504
  }
7032
- if (!fs18.existsSync(filePath) || !fs18.statSync(filePath).isFile()) continue;
7033
- const ext = path18.extname(filePath).toLowerCase();
7505
+ if (!fs19.existsSync(filePath) || !fs19.statSync(filePath).isFile()) continue;
7506
+ const ext = path19.extname(filePath).toLowerCase();
7034
7507
  if (imageExts.has(ext)) {
7035
7508
  try {
7036
- const stat = fs18.statSync(filePath);
7509
+ const stat = fs19.statSync(filePath);
7037
7510
  if (stat.size > maxImageBytes) {
7038
- process.stdout.write(pc7.yellow(` [skipped: ${path18.basename(filePath)} \u2014 exceeds 20MB limit]
7511
+ process.stdout.write(pc7.yellow(` [skipped: ${path19.basename(filePath)} \u2014 exceeds 20MB limit]
7039
7512
  `));
7040
7513
  continue;
7041
7514
  }
7042
- const data = fs18.readFileSync(filePath).toString("base64");
7515
+ const data = fs19.readFileSync(filePath).toString("base64");
7043
7516
  const mediaType = mimeMap[ext] || "image/png";
7044
7517
  imageBlocks.push({
7045
7518
  type: "image",
7046
7519
  source: { type: "base64", media_type: mediaType, data }
7047
7520
  });
7048
- process.stdout.write(pc7.dim(` [attached image: ${path18.basename(filePath)} (${(stat.size / 1024).toFixed(1)}KB)]
7521
+ process.stdout.write(pc7.dim(` [attached image: ${path19.basename(filePath)} (${(stat.size / 1024).toFixed(1)}KB)]
7049
7522
  `));
7050
7523
  } catch {
7051
7524
  process.stdout.write(pc7.dim(` [could not read image: ${filePath}]
@@ -7053,7 +7526,7 @@ ${knowledgeItem.content}
7053
7526
  }
7054
7527
  } else if (textExts.has(ext) || ext === "") {
7055
7528
  try {
7056
- const content = fs18.readFileSync(filePath, "utf-8");
7529
+ const content = fs19.readFileSync(filePath, "utf-8");
7057
7530
  const maxChars = 5e4;
7058
7531
  const trimmed = content.length > maxChars ? content.slice(0, maxChars) + `
7059
7532
 
@@ -7063,7 +7536,7 @@ ${knowledgeItem.content}
7063
7536
  <file path="${filePath}" size="${content.length} chars">
7064
7537
  ${trimmed}
7065
7538
  </file>`;
7066
- process.stdout.write(pc7.dim(` [attached: ${path18.basename(filePath)} (${(content.length / 1024).toFixed(1)}KB)]
7539
+ process.stdout.write(pc7.dim(` [attached: ${path19.basename(filePath)} (${(content.length / 1024).toFixed(1)}KB)]
7067
7540
  `));
7068
7541
  } catch {
7069
7542
  process.stdout.write(pc7.dim(` [could not read: ${filePath}]
@@ -7072,7 +7545,7 @@ ${trimmed}
7072
7545
  } else if (docExts.has(ext)) {
7073
7546
  if (mcpManager) {
7074
7547
  try {
7075
- process.stdout.write(pc7.dim(` [converting: ${path18.basename(filePath)}...]
7548
+ process.stdout.write(pc7.dim(` [converting: ${path19.basename(filePath)}...]
7076
7549
  `));
7077
7550
  const converted = await mcpManager.callTool("doc_convert", { path: filePath });
7078
7551
  if (converted && !converted.startsWith("Error") && !converted.includes("Could not convert")) {
@@ -7081,7 +7554,7 @@ ${trimmed}
7081
7554
  <file path="${filePath}" format="${ext}">
7082
7555
  ${converted.slice(0, 5e4)}
7083
7556
  </file>`;
7084
- process.stdout.write(pc7.dim(` [attached: ${path18.basename(filePath)} (converted from ${ext})]
7557
+ process.stdout.write(pc7.dim(` [attached: ${path19.basename(filePath)} (converted from ${ext})]
7085
7558
  `));
7086
7559
  } else {
7087
7560
  textContent += `
@@ -7093,7 +7566,7 @@ ${converted}
7093
7566
  `));
7094
7567
  }
7095
7568
  } catch {
7096
- process.stdout.write(pc7.dim(` [could not convert: ${path18.basename(filePath)}]
7569
+ process.stdout.write(pc7.dim(` [could not convert: ${path19.basename(filePath)}]
7097
7570
  `));
7098
7571
  }
7099
7572
  } else {
@@ -7397,7 +7870,7 @@ ${result2.response}` : `[${input2.profile}] failed: ${result2.error}`;
7397
7870
  }
7398
7871
  if (hooksConfig?.featureHints) {
7399
7872
  hintState.turnCount++;
7400
- const hasWorkflows = fs18.existsSync(path18.join(os17.homedir(), ".aflow", "flow.md"));
7873
+ const hasWorkflows = fs19.existsSync(path19.join(os18.homedir(), ".aflow", "flow.md"));
7401
7874
  const memoryCount = memoryTokens > 0 ? Math.floor(memoryTokens / 5) : 0;
7402
7875
  const hint = getHint(hintState, { hasWorkflows, memoryCount });
7403
7876
  if (hint) {
@@ -7443,9 +7916,9 @@ async function saveConversationToMemory(messages, sessionId) {
7443
7916
  }
7444
7917
 
7445
7918
  // src/index.ts
7446
- import fs19 from "fs";
7447
- import path19 from "path";
7448
- import os18 from "os";
7919
+ import fs20 from "fs";
7920
+ import path20 from "path";
7921
+ import os19 from "os";
7449
7922
 
7450
7923
  // src/presets.ts
7451
7924
  var PRESETS = {
@@ -7554,9 +8027,9 @@ ${wfSections}`;
7554
8027
 
7555
8028
  // src/index.ts
7556
8029
  async function autoDetectConfig() {
7557
- const reconfigMarker = path19.join(os18.homedir(), ".aman-agent", ".reconfig");
7558
- if (fs19.existsSync(reconfigMarker)) {
7559
- fs19.unlinkSync(reconfigMarker);
8030
+ const reconfigMarker = path20.join(os19.homedir(), ".aman-agent", ".reconfig");
8031
+ if (fs20.existsSync(reconfigMarker)) {
8032
+ fs20.unlinkSync(reconfigMarker);
7560
8033
  return null;
7561
8034
  }
7562
8035
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
@@ -7585,11 +8058,11 @@ async function autoDetectConfig() {
7585
8058
  return null;
7586
8059
  }
7587
8060
  function bootstrapEcosystem() {
7588
- const home2 = os18.homedir();
7589
- const corePath = path19.join(home2, ".acore", "core.md");
7590
- if (fs19.existsSync(corePath)) return false;
7591
- fs19.mkdirSync(path19.join(home2, ".acore"), { recursive: true });
7592
- fs19.writeFileSync(corePath, [
8061
+ const home2 = os19.homedir();
8062
+ const corePath = path20.join(home2, ".acore", "core.md");
8063
+ if (fs20.existsSync(corePath)) return false;
8064
+ fs20.mkdirSync(path20.join(home2, ".acore"), { recursive: true });
8065
+ fs20.writeFileSync(corePath, [
7593
8066
  "# Aman",
7594
8067
  "",
7595
8068
  "## Personality",
@@ -7601,11 +8074,11 @@ function bootstrapEcosystem() {
7601
8074
  "## Session",
7602
8075
  "_New companion \u2014 no prior sessions._"
7603
8076
  ].join("\n"), "utf-8");
7604
- const rulesDir = path19.join(home2, ".arules");
7605
- const rulesPath = path19.join(rulesDir, "rules.md");
7606
- if (!fs19.existsSync(rulesPath)) {
7607
- fs19.mkdirSync(rulesDir, { recursive: true });
7608
- fs19.writeFileSync(rulesPath, [
8077
+ const rulesDir = path20.join(home2, ".arules");
8078
+ const rulesPath = path20.join(rulesDir, "rules.md");
8079
+ if (!fs20.existsSync(rulesPath)) {
8080
+ fs20.mkdirSync(rulesDir, { recursive: true });
8081
+ fs20.writeFileSync(rulesPath, [
7609
8082
  "# Guardrails",
7610
8083
  "",
7611
8084
  "## safety",
@@ -7617,22 +8090,22 @@ function bootstrapEcosystem() {
7617
8090
  "- Respect the user's preferences stored in memory"
7618
8091
  ].join("\n"), "utf-8");
7619
8092
  }
7620
- const flowDir = path19.join(home2, ".aflow");
7621
- const flowPath = path19.join(flowDir, "flow.md");
7622
- if (!fs19.existsSync(flowPath)) {
7623
- fs19.mkdirSync(flowDir, { recursive: true });
7624
- fs19.writeFileSync(flowPath, "# Workflows\n\n_No workflows defined yet. Use /workflows add to create one._\n", "utf-8");
8093
+ const flowDir = path20.join(home2, ".aflow");
8094
+ const flowPath = path20.join(flowDir, "flow.md");
8095
+ if (!fs20.existsSync(flowPath)) {
8096
+ fs20.mkdirSync(flowDir, { recursive: true });
8097
+ fs20.writeFileSync(flowPath, "# Workflows\n\n_No workflows defined yet. Use /workflows add to create one._\n", "utf-8");
7625
8098
  }
7626
- const skillDir = path19.join(home2, ".askill");
7627
- const skillPath = path19.join(skillDir, "skills.md");
7628
- if (!fs19.existsSync(skillPath)) {
7629
- fs19.mkdirSync(skillDir, { recursive: true });
7630
- fs19.writeFileSync(skillPath, "# Skills\n\n_No skills installed yet. Use /skills install to add domain expertise._\n", "utf-8");
8099
+ const skillDir = path20.join(home2, ".askill");
8100
+ const skillPath = path20.join(skillDir, "skills.md");
8101
+ if (!fs20.existsSync(skillPath)) {
8102
+ fs20.mkdirSync(skillDir, { recursive: true });
8103
+ fs20.writeFileSync(skillPath, "# Skills\n\n_No skills installed yet. Use /skills install to add domain expertise._\n", "utf-8");
7631
8104
  }
7632
8105
  return true;
7633
8106
  }
7634
8107
  var program = new Command();
7635
- program.name("aman-agent").description("Your AI companion, running locally").version("0.25.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) => {
8108
+ program.name("aman-agent").description("Your AI companion, running locally").version("0.26.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
8109
  p3.intro(pc8.bold("aman agent") + pc8.dim(" \u2014 your AI companion"));
7637
8110
  let config = loadConfig();
7638
8111
  if (!config) {
@@ -7984,19 +8457,19 @@ program.command("init").description("Set up your AI companion with a guided wiza
7984
8457
  });
7985
8458
  if (p3.isCancel(preset)) process.exit(0);
7986
8459
  const result = applyPreset(preset, name || "Aman");
7987
- const home2 = os18.homedir();
7988
- fs19.mkdirSync(path19.join(home2, ".acore"), { recursive: true });
7989
- fs19.writeFileSync(path19.join(home2, ".acore", "core.md"), result.coreMd, "utf-8");
8460
+ const home2 = os19.homedir();
8461
+ fs20.mkdirSync(path20.join(home2, ".acore"), { recursive: true });
8462
+ fs20.writeFileSync(path20.join(home2, ".acore", "core.md"), result.coreMd, "utf-8");
7990
8463
  p3.log.success(`Identity created \u2014 ${PRESETS[preset].identity.personality.split(".")[0].toLowerCase()}`);
7991
8464
  if (result.rulesMd) {
7992
- fs19.mkdirSync(path19.join(home2, ".arules"), { recursive: true });
7993
- fs19.writeFileSync(path19.join(home2, ".arules", "rules.md"), result.rulesMd, "utf-8");
8465
+ fs20.mkdirSync(path20.join(home2, ".arules"), { recursive: true });
8466
+ fs20.writeFileSync(path20.join(home2, ".arules", "rules.md"), result.rulesMd, "utf-8");
7994
8467
  const ruleCount = (result.rulesMd.match(/^- /gm) || []).length;
7995
8468
  p3.log.success(`${ruleCount} rules set`);
7996
8469
  }
7997
8470
  if (result.flowMd) {
7998
- fs19.mkdirSync(path19.join(home2, ".aflow"), { recursive: true });
7999
- fs19.writeFileSync(path19.join(home2, ".aflow", "flow.md"), result.flowMd, "utf-8");
8471
+ fs20.mkdirSync(path20.join(home2, ".aflow"), { recursive: true });
8472
+ fs20.writeFileSync(path20.join(home2, ".aflow", "flow.md"), result.flowMd, "utf-8");
8000
8473
  const wfCount = (result.flowMd.match(/^## /gm) || []).length;
8001
8474
  p3.log.success(`${wfCount} workflow${wfCount > 1 ? "s" : ""} added`);
8002
8475
  }