@fickydev/pigent 0.1.21 → 0.1.22

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.22 - 2026-05-18
4
+
5
+ ### Added
6
+
7
+ - Added `/skills` command to list all known skills with active/inactive flags and source labels for the current chat session.
8
+ - Added session-scoped dynamic skill loading with `/skills add <skill>`, `/skills remove <skill>`, and `/skills clear`.
9
+ - Added current skill inventory to each agent prompt so users can ask natural questions like "what skills do you have?".
10
+ - Added `agent_sessions.session_skills` persistence and migration.
11
+
3
12
  ## 0.1.21 - 2026-05-18
4
13
 
5
14
  ### Fixed
package/TODO.md CHANGED
@@ -146,6 +146,9 @@
146
146
  - [x] Make README more end-user facing
147
147
  - [x] Confirm per-agent system prompt injection
148
148
  - [x] Confirm per-agent skills loading
149
+ - [x] Add `/skills` command for current session skills with active/inactive flags
150
+ - [x] Add session-scoped dynamic skill loading via `/skills add/remove/clear`
151
+ - [x] Add skill inventory context so agents can answer natural-language skill questions
149
152
  - [x] Confirm per-agent extensions loading
150
153
  - [ ] Confirm per-agent custom tools loading
151
154
  - [x] Prototype one prompt through Pi SDK
@@ -0,0 +1 @@
1
+ ALTER TABLE `agent_sessions` ADD `session_skills` text DEFAULT '[]' NOT NULL;
@@ -50,6 +50,13 @@
50
50
  "when": 1779117599104,
51
51
  "tag": "0006_flippant_bruce_banner",
52
52
  "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "6",
57
+ "when": 1779120000000,
58
+ "tag": "0007_session_skills",
59
+ "breakpoints": true
53
60
  }
54
61
  ]
55
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fickydev/pigent",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Autonomous multi-agent daemon using Pi as core execution engine.",
@@ -1,7 +1,9 @@
1
1
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import type { InboundMessage } from "../channels/types";
3
3
  import type { LoadedAgentConfig } from "../config/schemas";
4
+ import { hasSkillRef, listBuiltInSkills, parseSessionSkills, skillDisplayName, uniqueSkillRefs } from "../config/skillRefs";
4
5
  import type { Repositories } from "../db/repositories";
6
+ import type { AgentSessionRow } from "../db/schema";
5
7
  import { logger } from "../logging/logger";
6
8
  import { PiAgentRunner } from "../pi/PiAgentRunner";
7
9
  import { createTaskTools, type TaskToolContext } from "../pi/tools/taskTools";
@@ -117,8 +119,8 @@ export class AgentRunner {
117
119
  agent,
118
120
  profile: this.registry.getProfile(agent.profile),
119
121
  session,
120
- prompt: this.composePrompt(input, chatInstructions),
121
- customTools: this.buildCustomTools(agent, input.message),
122
+ prompt: this.composePrompt(input, session, chatInstructions),
123
+ customTools: this.buildCustomTools(agent, session, input.message),
122
124
  });
123
125
 
124
126
  if (session.piSessionId !== result.piSessionId || session.piSessionPath !== result.piSessionPath) {
@@ -145,10 +147,10 @@ export class AgentRunner {
145
147
  }
146
148
  }
147
149
 
148
- private buildCustomTools(agent: LoadedAgentConfig, message: InboundMessage): ToolDefinition[] | undefined {
150
+ private buildCustomTools(agent: LoadedAgentConfig, session: AgentSessionRow, message: InboundMessage): ToolDefinition[] | undefined {
149
151
  const tools: ToolDefinition[] = [];
150
152
 
151
- if (this.hasSkill(agent, "task-management") && this.scheduler) {
153
+ if (this.hasSkill(agent, session, "task-management") && this.scheduler) {
152
154
  const ctx: TaskToolContext = {
153
155
  agentId: agent.id,
154
156
  channel: message.channel,
@@ -173,18 +175,17 @@ export class AgentRunner {
173
175
  return tools.length > 0 ? tools : undefined;
174
176
  }
175
177
 
176
- private hasSkill(agent: LoadedAgentConfig, skillName: string): boolean {
178
+ private hasSkill(agent: LoadedAgentConfig, session: AgentSessionRow, skillName: string): boolean {
177
179
  const allSkills = [
178
180
  ...(agent.profile ? (this.registry.getProfile(agent.profile)?.defaultSkills ?? []) : []),
179
181
  ...agent.skills,
182
+ ...parseSessionSkills(session.sessionSkills),
180
183
  ];
181
- return allSkills.some((s) => {
182
- const base = s.split("/").pop() ?? s;
183
- return base === skillName || s === skillName;
184
- });
184
+
185
+ return hasSkillRef(allSkills, skillName);
185
186
  }
186
187
 
187
- private composePrompt(input: AgentRunInput, chatInstructions: string): string {
188
+ private composePrompt(input: AgentRunInput, session: AgentSessionRow, chatInstructions: string): string {
188
189
  const parts: string[] = [
189
190
  `[Channel]\n${input.message.channel}`,
190
191
  `[Chat]\n${input.message.chatId}`,
@@ -202,11 +203,48 @@ export class AgentRunner {
202
203
  parts.push(`[Chat Instructions]\n${chatInstructions}`);
203
204
  }
204
205
 
206
+ const skillsContext = this.skillsContext(input.agentId, session);
207
+ if (skillsContext) {
208
+ parts.push(`[Skills]\n${skillsContext}`);
209
+ }
210
+
205
211
  parts.push(`[Message]\n${input.text}`);
206
212
 
207
213
  return parts.join("\n\n");
208
214
  }
209
215
 
216
+ private skillsContext(agentId: string, session: AgentSessionRow): string {
217
+ const agent = this.registry.getAgent(agentId);
218
+ if (!agent) return "";
219
+
220
+ const profile = this.registry.getProfile(agent.profile);
221
+ const profileSkills = profile?.defaultSkills ?? [];
222
+ const agentSkills = agent.skills;
223
+ const sessionSkills = parseSessionSkills(session.sessionSkills);
224
+ const builtInSkills = listBuiltInSkills(import.meta.dir);
225
+ const activeSkills = uniqueSkillRefs([...profileSkills, ...agentSkills, ...sessionSkills]);
226
+ const knownSkills = uniqueSkillRefs([...builtInSkills, ...activeSkills]).sort((a, b) =>
227
+ skillDisplayName(a).localeCompare(skillDisplayName(b)),
228
+ );
229
+
230
+ if (knownSkills.length === 0) return "No skills are configured or available.";
231
+
232
+ return knownSkills
233
+ .map((skill) => {
234
+ const name = skillDisplayName(skill);
235
+ const active = hasSkillRef(activeSkills, name);
236
+ const sources = [];
237
+
238
+ if (hasSkillRef(profileSkills, name)) sources.push("profile");
239
+ if (hasSkillRef(agentSkills, name)) sources.push("agent");
240
+ if (hasSkillRef(sessionSkills, name)) sources.push("session");
241
+ if (hasSkillRef(builtInSkills, name)) sources.push("built-in");
242
+
243
+ return `- ${name}: ${active ? "active" : "available, inactive"}${sources.length > 0 ? ` (${sources.join(", ")})` : ""}`;
244
+ })
245
+ .join("\n");
246
+ }
247
+
210
248
  private async chatInstructions(input: AgentRunInput): Promise<string> {
211
249
  if (input.message.channel !== "telegram") return "";
212
250
 
@@ -1,6 +1,7 @@
1
1
  import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
2
2
  import type { InboundMessage, InlineKeyboardButton } from "../channels/types";
3
3
  import type { LoadedAgentConfig, LoadedProfileConfig, ModelChoiceConfig } from "../config/schemas";
4
+ import { hasSkillRef, listBuiltInSkills, parseSessionSkills, skillDisplayName, uniqueSkillRefs } from "../config/skillRefs";
4
5
  import type { AgentSessionRow } from "../db/schema";
5
6
  import type { Repositories } from "../db/repositories";
6
7
  import { PiAgentRunner, type PiContextUsage } from "../pi/PiAgentRunner";
@@ -47,6 +48,8 @@ export class BotCommandHandler {
47
48
  return { handled: true, text: await this.newSessionText(message) };
48
49
  case "/status":
49
50
  return { handled: true, text: await this.statusText(message) };
51
+ case "/skills":
52
+ return { handled: true, text: await this.skillsText(message) };
50
53
  case "/model":
51
54
  return await this.modelResult(message);
52
55
  case "/thinking":
@@ -71,6 +74,9 @@ export class BotCommandHandler {
71
74
  "/agents - list agents for this chat",
72
75
  "/new - start a fresh session for this chat",
73
76
  "/status - show current session status",
77
+ "/skills - list skills for this chat session",
78
+ "/skills add <skill> - load a skill for this session",
79
+ "/skills remove <skill> - unload a session-loaded skill",
74
80
  "/model - show model picker for this chat session",
75
81
  "/model <provider/modelId> - set model for this chat session",
76
82
  "/model default - clear model override for this chat session",
@@ -191,6 +197,83 @@ export class BotCommandHandler {
191
197
  };
192
198
  }
193
199
 
200
+ private async skillsText(message: InboundMessage): Promise<string> {
201
+ const sessionResult = await this.getDefaultSession(message);
202
+ if (!sessionResult.ok) return sessionResult.message;
203
+
204
+ const [, actionRaw, ...args] = message.text.trim().split(/\s+/);
205
+ const action = actionRaw?.toLowerCase() ?? "list";
206
+ const skillRef = args.join(" ").trim();
207
+
208
+ if (action === "add") {
209
+ if (!skillRef) return "Usage: /skills add <skill>";
210
+
211
+ const current = parseSessionSkills(sessionResult.session.sessionSkills);
212
+ const next = uniqueSkillRefs([...current, skillRef]);
213
+ await this.repositories.sessions.updateSessionSkills(sessionResult.session.id, next);
214
+
215
+ return [
216
+ "Skill loaded for current session: " + skillRef,
217
+ "",
218
+ "It will be included on the next agent response in this chat session.",
219
+ "Use /skills to see active skills.",
220
+ ].join("\n");
221
+ }
222
+
223
+ if (action === "remove") {
224
+ if (!skillRef) return "Usage: /skills remove <skill>";
225
+
226
+ const current = parseSessionSkills(sessionResult.session.sessionSkills);
227
+ const next = current.filter((item) => item !== skillRef);
228
+ await this.repositories.sessions.updateSessionSkills(sessionResult.session.id, next);
229
+
230
+ return next.length === current.length
231
+ ? "Session skill not found: " + skillRef
232
+ : "Skill removed from current session: " + skillRef;
233
+ }
234
+
235
+ if (action === "clear") {
236
+ await this.repositories.sessions.updateSessionSkills(sessionResult.session.id, []);
237
+ return "Session-loaded skills cleared.";
238
+ }
239
+
240
+ if (action !== "list") {
241
+ return [
242
+ "Usage:",
243
+ "/skills - list skills",
244
+ "/skills add <skill> - load skill for this session",
245
+ "/skills remove <skill> - unload session skill",
246
+ "/skills clear - clear session-loaded skills",
247
+ ].join("\n");
248
+ }
249
+
250
+ const agent = sessionResult.agent;
251
+ const profile = this.registry.getProfile(agent.profile);
252
+ const profileSkills = profile?.defaultSkills ?? [];
253
+ const agentSkills = agent.skills;
254
+ const sessionSkills = parseSessionSkills(sessionResult.session.sessionSkills);
255
+ const builtInSkills = listBuiltInSkills(import.meta.dir);
256
+ const activeSkills = uniqueSkillRefs([...profileSkills, ...agentSkills, ...sessionSkills]);
257
+ const knownSkills = uniqueSkillRefs([...builtInSkills, ...activeSkills]).sort((a, b) =>
258
+ skillDisplayName(a).localeCompare(skillDisplayName(b)),
259
+ );
260
+
261
+ return [
262
+ "Skills for current session",
263
+ "",
264
+ "Agent: " + agent.id,
265
+ "Session: " + sessionResult.session.id,
266
+ "",
267
+ "Available skills:",
268
+ ...formatSkillRows(knownSkills, { profileSkills, agentSkills, sessionSkills, builtInSkills }),
269
+ "",
270
+ "Legend: ✅ active, ⬜ available but inactive",
271
+ "Load on the fly: /skills add <skill>",
272
+ "Unload: /skills remove <skill>",
273
+ "Clear session-loaded: /skills clear",
274
+ ].join("\n");
275
+ }
276
+
194
277
  private async modelResult(message: InboundMessage): Promise<BotCommandResult> {
195
278
  const sessionResult = await this.getDefaultSession(message);
196
279
  if (!sessionResult.ok) return { handled: true, text: sessionResult.message };
@@ -511,6 +594,33 @@ function formatDuration(ms: number): string {
511
594
  return Math.floor(hours / 24) + "d " + (hours % 24) + "h";
512
595
  }
513
596
 
597
+ function formatSkillRows(
598
+ skills: string[],
599
+ sources: { profileSkills: string[]; agentSkills: string[]; sessionSkills: string[]; builtInSkills: string[] },
600
+ ): string[] {
601
+ if (skills.length === 0) return ["none"];
602
+
603
+ return skills.map((skill) => {
604
+ const name = skillDisplayName(skill);
605
+ const sourceLabels = [];
606
+
607
+ if (hasSkillRef(sources.profileSkills, name)) sourceLabels.push("profile");
608
+ if (hasSkillRef(sources.agentSkills, name)) sourceLabels.push("agent");
609
+ if (hasSkillRef(sources.sessionSkills, name)) sourceLabels.push("session");
610
+ if (hasSkillRef(sources.builtInSkills, name)) sourceLabels.push("built-in");
611
+
612
+ const active = sourceLabels.some((source) => source !== "built-in");
613
+ const marker = active ? "✅" : "⬜";
614
+ const sourceText = sourceLabels.length > 0 ? sourceLabels.join(", ") : "configured";
615
+
616
+ return `${marker} ${name} (${active ? "active" : "inactive"}; ${sourceText})`;
617
+ });
618
+ }
619
+
620
+ function formatList(items: string[]): string {
621
+ return items.length > 0 ? items.join(", ") : "none";
622
+ }
623
+
514
624
  function formatPath(path: string): string {
515
625
  const home = process.env.HOME || process.env.USERPROFILE;
516
626
  if (home && path.startsWith(home)) {
@@ -0,0 +1,50 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ export function parseSessionSkills(raw: string | null | undefined): string[] {
5
+ if (!raw) return [];
6
+
7
+ try {
8
+ const parsed = JSON.parse(raw);
9
+ if (!Array.isArray(parsed)) return [];
10
+
11
+ return parsed.filter((item): item is string => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
12
+ } catch {
13
+ return [];
14
+ }
15
+ }
16
+
17
+ export function skillDisplayName(ref: string): string {
18
+ const normalized = ref.replace(/\\/g, "/").replace(/\/$/, "");
19
+ return normalized.split("/").pop() || ref;
20
+ }
21
+
22
+ export function hasSkillRef(refs: string[], skillName: string): boolean {
23
+ return refs.some((ref) => ref === skillName || skillDisplayName(ref) === skillName);
24
+ }
25
+
26
+ export function uniqueSkillRefs(refs: string[]): string[] {
27
+ const seen = new Set<string>();
28
+ const unique: string[] = [];
29
+
30
+ for (const ref of refs.map((item) => item.trim()).filter(Boolean)) {
31
+ if (seen.has(ref)) continue;
32
+ seen.add(ref);
33
+ unique.push(ref);
34
+ }
35
+
36
+ return unique;
37
+ }
38
+
39
+ export function listBuiltInSkills(fromDir: string): string[] {
40
+ const skillsDir = resolve(fromDir, "../../skills");
41
+
42
+ if (!existsSync(skillsDir)) return [];
43
+
44
+ return readdirSync(skillsDir)
45
+ .filter((name) => {
46
+ const path = resolve(skillsDir, name);
47
+ return statSync(path).isDirectory() && existsSync(resolve(path, "SKILL.md"));
48
+ })
49
+ .sort();
50
+ }
@@ -181,6 +181,7 @@ function telegramCommands() {
181
181
  { command: "agents", description: "List agents available in this chat" },
182
182
  { command: "new", description: "Start a fresh chat session" },
183
183
  { command: "status", description: "Show current session status" },
184
+ { command: "skills", description: "List or load skills for this session" },
184
185
  { command: "model", description: "Choose model for this chat session" },
185
186
  { command: "thinking", description: "Choose thinking level for this chat session" },
186
187
  { command: "agent", description: "Send message to a specific agent" },
package/src/db/client.ts CHANGED
@@ -30,6 +30,7 @@ function ensureRuntimeSchema(): void {
30
30
  ensureColumn("agent_sessions", "active", "ALTER TABLE `agent_sessions` ADD `active` integer DEFAULT true NOT NULL");
31
31
  ensureColumn("agent_sessions", "ended_at", "ALTER TABLE `agent_sessions` ADD `ended_at` integer");
32
32
  ensureColumn("agent_sessions", "pi_session_path", "ALTER TABLE `agent_sessions` ADD `pi_session_path` text");
33
+ ensureColumn("agent_sessions", "session_skills", "ALTER TABLE `agent_sessions` ADD `session_skills` text DEFAULT '[]' NOT NULL");
33
34
  }
34
35
 
35
36
  function ensureColumn(table: string, column: string, statement: string): void {
@@ -75,6 +75,18 @@ export class SessionRepository {
75
75
  return this.requireById(id);
76
76
  }
77
77
 
78
+ async updateSessionSkills(id: string, skills: string[]): Promise<AgentSessionRow> {
79
+ await this.db
80
+ .update(agentSessions)
81
+ .set({
82
+ sessionSkills: JSON.stringify(skills),
83
+ updatedAt: Date.now(),
84
+ })
85
+ .where(eq(agentSessions.id, id));
86
+
87
+ return this.requireById(id);
88
+ }
89
+
78
90
  async findById(id: string): Promise<AgentSessionRow | null> {
79
91
  const row = await this.db.query.agentSessions.findFirst({
80
92
  where: eq(agentSessions.id, id),
package/src/db/schema.ts CHANGED
@@ -51,6 +51,7 @@ export const agentSessions = sqliteTable(
51
51
  instructionsHash: text("instructions_hash"),
52
52
  model: text("model"),
53
53
  thinkingLevel: text("thinking_level", { enum: ["off", "low", "medium", "high"] }),
54
+ sessionSkills: text("session_skills").notNull().default("[]"),
54
55
  active: integer("active", { mode: "boolean" }).notNull().default(true),
55
56
  endedAt: integer("ended_at"),
56
57
  createdAt: integer("created_at").notNull(),
@@ -10,6 +10,7 @@ import {
10
10
  import { mkdir } from "node:fs/promises";
11
11
  import { resolve } from "node:path";
12
12
  import type { LoadedAgentConfig, LoadedProfileConfig } from "../config/schemas";
13
+ import { parseSessionSkills } from "../config/skillRefs";
13
14
  import type { AgentSessionRow } from "../db/schema";
14
15
  import { resolveModelSelection } from "./PiModelResolver";
15
16
  import { loadOrCreatePiSession } from "./PiSessionFactory";
@@ -82,7 +83,7 @@ export class PiAgentRunner {
82
83
  compaction: { enabled: false },
83
84
  });
84
85
  const agentDir = getAgentDir();
85
- const skillPaths = resolveSkillPaths(input.agent, input.profile);
86
+ const skillPaths = resolveSkillPaths(input.agent, input.profile, input.session);
86
87
  const resourceLoader = new DefaultResourceLoader({
87
88
  cwd: workspace,
88
89
  agentDir,
@@ -114,7 +115,7 @@ export class PiAgentRunner {
114
115
  }
115
116
  }
116
117
 
117
- function resolveSkillPaths(agent: LoadedAgentConfig, profile: LoadedProfileConfig | null): string[] {
118
+ function resolveSkillPaths(agent: LoadedAgentConfig, profile: LoadedProfileConfig | null, session: AgentSessionRow): string[] {
118
119
  const paths: string[] = [];
119
120
 
120
121
  if (profile) {
@@ -127,6 +128,10 @@ function resolveSkillPaths(agent: LoadedAgentConfig, profile: LoadedProfileConfi
127
128
  paths.push(resolveSkillPath(s, agent.baseDir));
128
129
  }
129
130
 
131
+ for (const s of parseSessionSkills(session.sessionSkills)) {
132
+ paths.push(resolveSkillPath(s, agent.baseDir));
133
+ }
134
+
130
135
  return paths;
131
136
  }
132
137