assistme 0.3.4 → 0.3.6

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.
@@ -1,6 +1,13 @@
1
1
  import { execSync } from "child_process";
2
2
  import { log } from "../utils/logger.js";
3
3
  import { callMcpHandler } from "../db/api-client.js";
4
+ import { SKILL_DESCRIPTION_BUDGET_CHARS } from "../utils/constants.js";
5
+ import {
6
+ SkillRowSchema,
7
+ SkillCreateResultSchema,
8
+ BrowseSkillRowSchema,
9
+ safeParse,
10
+ } from "../utils/schemas.js";
4
11
 
5
12
  // ── Text Processing Helpers ─────────────────────────────────────────
6
13
 
@@ -209,7 +216,7 @@ export class SkillManager {
209
216
  /** Cache for findRelevant() — keyed by prompt, invalidated on skill changes */
210
217
  private relevanceCache: Map<string, { results: Skill[]; maxResults: number }> = new Map();
211
218
 
212
- private readonly DESCRIPTION_BUDGET_CHARS = 16_000;
219
+ private readonly DESCRIPTION_BUDGET_CHARS = SKILL_DESCRIPTION_BUDGET_CHARS;
213
220
 
214
221
  setUserId(userId: string): void {
215
222
  this.userId = userId;
@@ -219,10 +226,12 @@ export class SkillManager {
219
226
  if (!this.userId) return;
220
227
 
221
228
  try {
222
- const data = await callMcpHandler<Record<string, unknown>[]>("skill.load");
229
+ const data = await callMcpHandler<unknown[]>("skill.load");
223
230
 
224
231
  this.skills.clear();
225
- for (const row of data || []) {
232
+ for (const raw of data || []) {
233
+ const row = safeParse(SkillRowSchema, raw);
234
+ if (!row) continue;
226
235
  const skill = this.rowToSkill(row);
227
236
  this.skills.set(skill.name, skill);
228
237
  }
@@ -239,22 +248,22 @@ export class SkillManager {
239
248
 
240
249
  private rowToSkill(row: Record<string, unknown>): Skill {
241
250
  return {
242
- name: row.name as string,
243
- description: (row.description as string) || "",
244
- version: (row.version as string) || "1.0.0",
251
+ name: String(row.name),
252
+ description: String(row.description ?? ""),
253
+ version: String(row.version ?? "1.0.0"),
245
254
  userInvocable: row.user_invocable !== false,
246
255
  disableModelInvocation: row.disable_model_invocation === true,
247
- keywords: (row.keywords as string[]) || [],
248
- allowedTools: (row.allowed_tools as string[]) || [],
249
- argumentHint: (row.argument_hint as string) || "",
256
+ keywords: Array.isArray(row.keywords) ? row.keywords : [],
257
+ allowedTools: Array.isArray(row.allowed_tools) ? row.allowed_tools : [],
258
+ argumentHint: String(row.argument_hint ?? ""),
250
259
  metadata: parseDbMetadata(row.metadata),
251
- homepage: (row.homepage as string) || "",
252
- content: row.content as string,
260
+ homepage: String(row.homepage ?? ""),
261
+ content: String(row.content ?? ""),
253
262
  filePath: "",
254
263
  source: (row.source as Skill["source"]) || "manual",
255
- dbId: row.id as string,
256
- sourceSkillId: (row.source_skill_id as string) || undefined,
257
- invocationCount: (row.invocation_count as number) || 0,
264
+ dbId: row.id != null ? String(row.id) : undefined,
265
+ sourceSkillId: row.source_skill_id != null ? String(row.source_skill_id) : undefined,
266
+ invocationCount: typeof row.invocation_count === "number" ? row.invocation_count : 0,
258
267
  };
259
268
  }
260
269
 
@@ -419,14 +428,15 @@ export class SkillManager {
419
428
  }
420
429
  );
421
430
 
422
- const row = (Array.isArray(data) ? data[0] : data) as Record<string, unknown> | null;
431
+ const raw = Array.isArray(data) ? data[0] : data;
432
+ const row = safeParse(SkillCreateResultSchema, raw);
423
433
  if (!row) {
424
- log.debug(`Skill create returned no data for "${name}"`);
434
+ log.debug(`Skill create returned invalid data for "${name}"`);
425
435
  return null;
426
436
  }
427
437
 
428
- const id = (row.out_id || row.id) as string;
429
- const skillName = (row.out_name || row.name) as string;
438
+ const id = row.out_id || row.id!;
439
+ const skillName = row.out_name || row.name || name;
430
440
 
431
441
  // Add to in-memory map so the skill is immediately searchable
432
442
  this.skills.set(skillName, {
@@ -658,11 +668,11 @@ export class SkillManager {
658
668
  });
659
669
  if (data) {
660
670
  return data.map((row) => ({
661
- name: row.name as string,
662
- description: (row.description as string) || "",
663
- emoji: (row.emoji as string) || "",
664
- source: (row.source as string) || "manual",
665
- invocationCount: (row.invocation_count as number) || 0,
671
+ name: String(row.name),
672
+ description: String(row.description ?? ""),
673
+ emoji: String(row.emoji ?? ""),
674
+ source: String(row.source ?? "manual"),
675
+ invocationCount: typeof row.invocation_count === "number" ? row.invocation_count : 0,
666
676
  }));
667
677
  }
668
678
  } catch {
@@ -757,18 +767,21 @@ export class SkillManager {
757
767
  offset: options?.offset || 0,
758
768
  });
759
769
 
760
- return (data || []).map((r) => ({
761
- id: r.id as string,
762
- name: r.name as string,
763
- description: (r.description as string) || "",
764
- emoji: (r.emoji as string) || "",
765
- version: (r.version as string) || "1.0.0",
766
- authorName: (r.author_name as string) || "",
767
- category: (r.category as string) || "",
768
- installCount: (r.install_count as number) || 0,
769
- avgRating: r.avg_rating != null ? (r.avg_rating as number) : null,
770
- ratingCount: (r.rating_count as number) || 0,
771
- }));
770
+ return (data || [])
771
+ .map((r) => safeParse(BrowseSkillRowSchema, r))
772
+ .filter(Boolean)
773
+ .map((r) => ({
774
+ id: r!.id,
775
+ name: r!.name,
776
+ description: r!.description,
777
+ emoji: r!.emoji,
778
+ version: r!.version,
779
+ authorName: r!.author_name,
780
+ category: r!.category,
781
+ installCount: r!.install_count,
782
+ avgRating: r!.avg_rating ?? null,
783
+ ratingCount: r!.rating_count,
784
+ }));
772
785
  } catch {
773
786
  return [];
774
787
  }
@@ -814,8 +827,16 @@ export function substituteArguments(content: string, args: string): string {
814
827
  return content;
815
828
  }
816
829
 
830
+ /** Safe commands allowed in dynamic context preprocessing */
831
+ const SAFE_DYNAMIC_COMMANDS =
832
+ /^(date|whoami|hostname|uname|pwd|echo|node\s+--version|npm\s+--version|git\s+(branch|rev-parse|log\s+--oneline)|cat\s+)/;
833
+
817
834
  export function preprocessDynamicContext(content: string, cwd?: string): string {
818
- return content.replace(/!`([^`]+)`/g, (_, cmd) => {
835
+ return content.replace(/!`([^`]+)`/g, (_, cmd: string) => {
836
+ // Only allow safe, read-only commands to prevent command injection
837
+ if (!SAFE_DYNAMIC_COMMANDS.test(cmd.trim())) {
838
+ return `[command blocked: ${cmd}]`;
839
+ }
819
840
  try {
820
841
  return execSync(cmd, { timeout: 10_000, encoding: "utf-8", cwd }).trim();
821
842
  } catch {