agent-from-zero 0.35.2 → 0.35.4

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/skills.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs"; // discover and read SKILL.md files
2
2
  import os from "node:os"; // the global skills dir
3
3
  import path from "node:path"; // path resolution
4
- import { CONFIG } from "./config.js"; // the context window sizes the listing budget
4
+ import { CONFIG, saveGlobalSetting } from "./config.js"; // the listing budget + /skills on/off choices
5
5
  import { registerExternalTool, tools, unregisterExternalTool } from "./tools.js"; // a skill is exposed to the model as a tool
6
6
  // Project skills win over global ones with the same name.
7
7
  function skillDirs() {
@@ -37,6 +37,7 @@ export function parseSkill(raw, fallbackName, filePath = "") {
37
37
  .filter(Boolean),
38
38
  disableModelInvocation: /^true$/i.test(fm.disableModelInvocation || fm["disable-model-invocation"] || ""),
39
39
  argumentHint: fm["argument-hint"] || "",
40
+ argNames: (fm.arguments || "").replace(/^\[|\]$/g, "").split(/[\s,]+/).map((a) => a.replace(/^["']|["']$/g, "")).filter(Boolean), // "a, b" or "[a, b]"
40
41
  body,
41
42
  path: filePath,
42
43
  dir: filePath ? path.dirname(filePath) : "",
@@ -100,19 +101,111 @@ function fingerprint(dirs) {
100
101
  }
101
102
  return parts.join("|");
102
103
  }
103
- // The live skill set: reloaded from disk only when a SKILL.md was added,
104
- // edited, or removed since the last call. `dirs` is injectable for tests.
105
- export function currentSkills(dirs = skillDirs()) {
104
+ // Every skill on disk, whatever its /skills setting: reloaded only when a
105
+ // SKILL.md was added, edited, or removed since the last call. `dirs` is
106
+ // injectable for tests. The /skills panel lists these.
107
+ export function allSkills(dirs = skillDirs()) {
106
108
  const key = dirs.join("\n");
107
109
  const fp = fingerprint(dirs);
108
110
  if (!cache || cache.key !== key || cache.fingerprint !== fp)
109
111
  cache = { key, fingerprint: fp, skills: loadSkills(dirs) };
110
112
  return cache.skills;
111
113
  }
114
+ // The skills in effect — what the listing, the skill tool and /skill see.
115
+ // The /skills choices apply here: "off" skills vanish entirely, "user-only"
116
+ // ones stay runnable by the user but are hidden from the model.
117
+ export function currentSkills(dirs = skillDirs()) {
118
+ return allSkills(dirs)
119
+ .filter((s) => skillMode(s) !== "off")
120
+ .map((s) => (skillMode(s) === "user-only" && !s.disableModelInvocation ? { ...s, disableModelInvocation: true } : s));
121
+ }
122
+ export const skillLocked = (s) => s.disableModelInvocation;
123
+ export function skillMode(s) {
124
+ if (skillLocked(s))
125
+ return "user-only";
126
+ return CONFIG.skillOverrides[s.name] ?? "on";
127
+ }
128
+ // Enter/space in the panel: on → user-only → off → on.
129
+ export const nextSkillMode = (m) => (m === "on" ? "user-only" : m === "user-only" ? "off" : "on");
130
+ // Apply and persist a choice. "on" is the default, so it removes the entry
131
+ // rather than storing it — the settings file only lists exceptions.
132
+ export function setSkillMode(name, mode, persist = true) {
133
+ const next = { ...CONFIG.skillOverrides };
134
+ if (mode === "on")
135
+ delete next[name];
136
+ else
137
+ next[name] = mode;
138
+ CONFIG.skillOverrides = next;
139
+ if (persist)
140
+ saveGlobalSetting("skillOverrides", next);
141
+ }
142
+ // One panel row's facts: where it lives and what it costs the model to know
143
+ // about it (its listing line, ~4 bytes per token — the same estimate as context.ts).
144
+ export function skillSource(s) {
145
+ return s.path.startsWith(path.resolve(".mini-agent", "skills")) ? "project" : "user";
146
+ }
147
+ export const skillListingTokens = (s) => Math.ceil(Buffer.byteLength(listingEntry(s)) / 4);
148
+ export const nextSkillSort = (s) => (s === "name" ? "tokens" : s === "tokens" ? "source" : "name");
149
+ // The panel's list: filtered by the search box, sorted.
150
+ // Matching is about the NAME: any part of it ("pul" finds co_dev_pulse). The
151
+ // description only counts for a real word — 3+ characters that start one of
152
+ // its words ("standup", "pulse") — and those rows come after the name matches.
153
+ // A plain substring over descriptions matched everything: a single "a" is in
154
+ // almost every sentence.
155
+ export function skillPanelRows(skills, query, sort) {
156
+ const q = query.trim().toLowerCase();
157
+ const byName = (a, b) => a.name.localeCompare(b.name);
158
+ const order = (rows) => sort === "tokens" ? rows.sort((a, b) => skillListingTokens(b) - skillListingTokens(a) || byName(a, b))
159
+ : sort === "source" ? rows.sort((a, b) => skillSource(a).localeCompare(skillSource(b)) || byName(a, b))
160
+ : rows.sort(byName);
161
+ if (!q)
162
+ return order([...skills]);
163
+ const inName = skills.filter((s) => s.name.toLowerCase().includes(q));
164
+ const inWords = q.length >= 3 ? skills.filter((s) => !inName.includes(s) && s.description.toLowerCase().split(/[^a-z0-9]+/).some((w) => w.startsWith(q))) : [];
165
+ return [...order(inName), ...order(inWords)];
166
+ }
112
167
  export function findSkill(skills, name) {
113
168
  const n = name.trim().replace(/^\//, "").toLowerCase(); // "/deploy" and "deploy" both work, like Claude Code
114
169
  return skills.find((s) => s.name.toLowerCase() === n);
115
170
  }
171
+ // ---- usage: what the "/" typeahead puts first -----------------------------------------
172
+ // Claude Code ranks a bare "/" by how much you use each skill: every run you
173
+ // start yourself (/name or /skill name) counts, and old use fades — the score
174
+ // halves every 7 days, never below a tenth. Kept in its own small file (not in
175
+ // settings.json, which is for choices, not counters).
176
+ // MINI_AGENT_SKILL_USAGE relocates it (tests point it at a scratch file).
177
+ const usagePath = () => process.env.MINI_AGENT_SKILL_USAGE || path.join(os.homedir(), ".config", "mini-agent", "skill-usage.json");
178
+ function readUsage() {
179
+ try {
180
+ return JSON.parse(fs.readFileSync(usagePath(), "utf8"));
181
+ }
182
+ catch {
183
+ return {}; // none yet, or unreadable — ranking just falls back to groups + names
184
+ }
185
+ }
186
+ export function recordSkillUsage(name, now = Date.now()) {
187
+ const usage = readUsage();
188
+ usage[name] = { usageCount: (usage[name]?.usageCount ?? 0) + 1, lastUsedAt: now };
189
+ try {
190
+ fs.mkdirSync(path.dirname(usagePath()), { recursive: true });
191
+ fs.writeFileSync(usagePath(), JSON.stringify(usage, null, 2) + "\n");
192
+ }
193
+ catch {
194
+ /* a counter is never worth failing a skill run over */
195
+ }
196
+ }
197
+ // One read of the usage file, scored per name — for the typeahead, which asks about every skill per keystroke.
198
+ export function skillUsageScores(now = Date.now()) {
199
+ const usage = readUsage();
200
+ return (name) => skillUsageScore(name, now, usage);
201
+ }
202
+ export function skillUsageScore(name, now = Date.now(), usage = readUsage()) {
203
+ const u = usage[name];
204
+ if (!u)
205
+ return 0;
206
+ const days = (now - u.lastUsedAt) / 86_400_000;
207
+ return u.usageCount * Math.max(0.5 ** (days / 7), 0.1);
208
+ }
116
209
  // ---- the listing, as a <system-reminder> ------------------------------------------
117
210
  export const wrapInSystemReminder = (content) => `<system-reminder>\n${content}\n</system-reminder>`;
118
211
  const LISTING_HEADER = "The following skills are available for use with the skill tool:"; // Claude Code's wording, our tool name
package/dist/title.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { currentSession, nameFromTitle, nameIsPinned, setSessionInfo } from "./peers.js"; // the session name goes on the tab; the title goes to /peers
1
2
  // One cheap model call that turns the user's first prompt into a short,
2
3
  // pickable session name — the same idea Claude Code uses (a Haiku-tier call
3
4
  // producing a 3-7 word sentence-case title, stored as the session's aiTitle).
@@ -18,10 +19,49 @@ export function terminalTitleSequence(title) {
18
19
  }
19
20
  // Rename the terminal tab. Silent when stdout isn't a terminal (pipes, tests,
20
21
  // print mode) — an escape sequence in a log file is just noise.
22
+ //
23
+ // With peer sessions the tab must tell windows apart, and it must match what
24
+ // /peers lists. So: the title alone ("✳ Fix login bug" — the session name is
25
+ // derived from it), the name before there is a title ("✳ mini-agent-2"), and
26
+ // "name · title" only when you chose the name yourself with /rename.
27
+ let lastTitle; // remembered so a rename (or the end of a flash) can redraw it
28
+ let flashing = null;
29
+ export function tabTitle(title, name = currentSession()?.name, pinned = nameIsPinned()) {
30
+ if (title)
31
+ return `${TAB_PREFIX}${pinned && name ? `${name} · ` : ""}${title}`;
32
+ return name ? `${TAB_PREFIX}${name}` : "mini-agent";
33
+ }
21
34
  export function setTerminalTitle(title) {
35
+ lastTitle = title;
36
+ nameFromTitle(title); // the session is called what it's doing (unless you named it)
37
+ setSessionInfo({ title: title ?? "" }); // other sessions' /peers show it too
38
+ if (!process.stdout.isTTY || flashing)
39
+ return;
40
+ process.stdout.write(terminalTitleSequence(tabTitle(title)));
41
+ }
42
+ export const refreshTerminalTitle = () => setTerminalTitle(lastTitle);
43
+ // "Which window is this?" — another session asked. Ring the bell and blink the
44
+ // tab title for a few seconds, then put the real title back.
45
+ export function flashTerminalTitle(ms = 6000) {
22
46
  if (!process.stdout.isTTY)
23
47
  return;
24
- process.stdout.write(terminalTitleSequence(title ? `${TAB_PREFIX}${title}` : "mini-agent"));
48
+ process.stdout.write("\x07");
49
+ const name = currentSession()?.name ?? "mini-agent";
50
+ let on = false;
51
+ if (flashing)
52
+ clearInterval(flashing);
53
+ const stopAt = Date.now() + ms;
54
+ flashing = setInterval(() => {
55
+ on = !on;
56
+ if (Date.now() >= stopAt) {
57
+ clearInterval(flashing);
58
+ flashing = null;
59
+ process.stdout.write(terminalTitleSequence(tabTitle(lastTitle)));
60
+ return;
61
+ }
62
+ process.stdout.write(terminalTitleSequence(on ? `👋 ${name} — HERE 👋` : tabTitle(lastTitle)));
63
+ }, 500);
64
+ flashing.unref();
25
65
  }
26
66
  // The prompt is quoted inside <prompt> tags and the system message says so
27
67
  // explicitly: sent bare, a chat-tuned model reads "有没有app能…?" as a question
package/dist/ui.js CHANGED
@@ -117,6 +117,21 @@ export function spinnerText(word, elapsedSec, subAgent, tokens, thinking) {
117
117
  bits.push("Ctrl+C to interrupt");
118
118
  return `${head}… ${chalk.dim(`(${bits.join(" · ")})`)}`;
119
119
  }
120
+ // The compaction progress bar, Claude Code's look:
121
+ // Compacting conversation…
122
+ // ▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 8%
123
+ // The two lines travel as ONE status text (a spinner carries one string); the
124
+ // Ink spinner draws the part after the newline under the verb, ora prints it as
125
+ // a second line. ▰/▱ are single-width everywhere, so the bar never jitters.
126
+ export function progressBar(pct, width = 40) {
127
+ const p = Math.max(0, Math.min(100, Math.floor(pct)));
128
+ const filled = Math.round((p / 100) * width);
129
+ return chalk.white("▰".repeat(filled)) + chalk.dim("▱".repeat(width - filled)) + chalk.dim(` ${p}%`);
130
+ }
131
+ export const COMPACTING_VERB = "Compacting conversation…";
132
+ export function compactingText(pct, columns = process.stdout.columns || 80) {
133
+ return `${COMPACTING_VERB}\n${progressBar(pct, Math.max(10, Math.min(40, columns - 10)))}`;
134
+ }
120
135
  // The persistent status line — model, project, branch, context use, spend, time.
121
136
  // Pure (dir + branch passed in) so it's testable without a filesystem or git.
122
137
  export function statusLine(model, dir, branch, ctxPct, costUsd, elapsedMs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-from-zero",
3
- "version": "0.35.2",
3
+ "version": "0.35.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mini-agent": "dist/agent.js"
@@ -17,7 +17,7 @@
17
17
  "auto:report": "tsx eval/auto-report.ts",
18
18
  "build": "tsc -p tsconfig.build.json",
19
19
  "prepare": "npm run build",
20
- "test": "export MINI_AGENT_REVIEW_DEBUG=0 && tsx tests/tools.test.ts && tsx tests/permissions.test.ts && tsx tests/context.test.ts && tsx tests/hooks.test.ts && tsx tests/mcp.test.ts && tsx tests/judge.test.ts && tsx tests/auto.test.ts && tsx tests/auto-context.test.ts && tsx tests/auto-providers.test.ts && tsx tests/auto-cascade.test.ts && tsx tests/auto-rules.test.ts && tsx tests/auto-rule-cascade.test.ts && tsx tests/auto-debug.test.ts && tsx tests/auto-report.test.ts && tsx tests/auto-clarification.test.ts && tsx tests/memory.test.ts && tsx tests/cost.test.ts && tsx tests/cli.test.ts && tsx tests/print.test.ts && tsx tests/status.test.ts && tsx tests/validate.test.ts && tsx tests/plan.test.ts && tsx tests/diff.test.ts && tsx tests/undo.test.ts && tsx tests/tiering.test.ts && tsx tests/mcp-http.test.ts && tsx tests/mcp-oauth.test.ts && tsx tests/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/title.test.ts && tsx tests/ui.test.ts && tsx tests/activity.test.ts && tsx tests/follow-up.test.ts && tsx tests/follow-up-ui.test.tsx && tsx tests/agents.test.ts && tsx tests/images.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/effort.test.ts && tsx tests/completion.test.ts && tsx tests/pasted-text.test.ts && tsx tests/skills.test.ts && tsx tests/markdown.test.ts && tsx tests/editor.test.ts && tsx tests/todos.test.ts && tsx tests/background.test.ts && tsx tests/team.test.ts && tsx tests/board.test.ts && tsx tests/drop.test.ts && tsx tests/editline.test.ts && tsx tests/cron.test.ts"
20
+ "test": "export MINI_AGENT_REVIEW_DEBUG=0 && tsx tests/tools.test.ts && tsx tests/permissions.test.ts && tsx tests/context.test.ts && tsx tests/hooks.test.ts && tsx tests/mcp.test.ts && tsx tests/judge.test.ts && tsx tests/auto.test.ts && tsx tests/auto-context.test.ts && tsx tests/auto-providers.test.ts && tsx tests/auto-cascade.test.ts && tsx tests/auto-rules.test.ts && tsx tests/auto-rule-cascade.test.ts && tsx tests/auto-debug.test.ts && tsx tests/auto-report.test.ts && tsx tests/auto-clarification.test.ts && tsx tests/memory.test.ts && tsx tests/cost.test.ts && tsx tests/cli.test.ts && tsx tests/print.test.ts && tsx tests/status.test.ts && tsx tests/validate.test.ts && tsx tests/plan.test.ts && tsx tests/diff.test.ts && tsx tests/undo.test.ts && tsx tests/tiering.test.ts && tsx tests/mcp-http.test.ts && tsx tests/mcp-oauth.test.ts && tsx tests/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/title.test.ts && tsx tests/ui.test.ts && tsx tests/activity.test.ts && tsx tests/follow-up.test.ts && tsx tests/follow-up-ui.test.tsx && tsx tests/agents.test.ts && tsx tests/images.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/effort.test.ts && tsx tests/completion.test.ts && tsx tests/pasted-text.test.ts && tsx tests/skills.test.ts && tsx tests/markdown.test.ts && tsx tests/editor.test.ts && tsx tests/todos.test.ts && tsx tests/background.test.ts && tsx tests/team.test.ts && tsx tests/peers.test.ts && tsx tests/board.test.ts && tsx tests/drop.test.ts && tsx tests/editline.test.ts && tsx tests/cron.test.ts"
21
21
  },
22
22
  "engines": {
23
23
  "node": ">=20.6"
@@ -26,6 +26,7 @@
26
26
  "dependencies": {
27
27
  "chalk": "^5.6.2",
28
28
  "cli-table3": "^0.6.5",
29
+ "fuse.js": "^7.5.0",
29
30
  "ink": "^5.2.1",
30
31
  "marked": "^15.0.12",
31
32
  "marked-terminal": "^7.3.0",