@oyasmi/pipiclaw 0.6.2 → 0.6.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.
Files changed (66) hide show
  1. package/README.md +5 -3
  2. package/dist/agent/channel-runner.d.ts +3 -0
  3. package/dist/agent/channel-runner.js +51 -0
  4. package/dist/agent/prompt-builder.js +4 -0
  5. package/dist/agent/session-events.d.ts +1 -0
  6. package/dist/agent/session-events.js +13 -1
  7. package/dist/agent/types.d.ts +2 -0
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +1 -1
  10. package/dist/log.js +25 -22
  11. package/dist/memory/channel-maintenance-queue.d.ts +5 -0
  12. package/dist/memory/channel-maintenance-queue.js +8 -0
  13. package/dist/memory/consolidation.d.ts +12 -4
  14. package/dist/memory/consolidation.js +54 -23
  15. package/dist/memory/files.js +8 -14
  16. package/dist/memory/lifecycle.d.ts +8 -14
  17. package/dist/memory/lifecycle.js +66 -111
  18. package/dist/memory/maintenance-gates.d.ts +56 -0
  19. package/dist/memory/maintenance-gates.js +161 -0
  20. package/dist/memory/maintenance-jobs.d.ts +52 -0
  21. package/dist/memory/maintenance-jobs.js +310 -0
  22. package/dist/memory/maintenance-state.d.ts +33 -0
  23. package/dist/memory/maintenance-state.js +113 -0
  24. package/dist/memory/post-turn-review.d.ts +32 -0
  25. package/dist/memory/post-turn-review.js +244 -0
  26. package/dist/memory/promotion-signals.d.ts +5 -0
  27. package/dist/memory/promotion-signals.js +34 -0
  28. package/dist/memory/promotion.d.ts +32 -0
  29. package/dist/memory/promotion.js +11 -0
  30. package/dist/memory/recall.d.ts +1 -1
  31. package/dist/memory/recall.js +33 -1
  32. package/dist/memory/review-log.d.ts +13 -0
  33. package/dist/memory/review-log.js +38 -0
  34. package/dist/memory/scheduler.d.ts +52 -0
  35. package/dist/memory/scheduler.js +152 -0
  36. package/dist/memory/session-corpus.d.ts +18 -0
  37. package/dist/memory/session-corpus.js +257 -0
  38. package/dist/memory/session-search.d.ts +30 -0
  39. package/dist/memory/session-search.js +151 -0
  40. package/dist/runtime/bootstrap.d.ts +5 -0
  41. package/dist/runtime/bootstrap.js +37 -0
  42. package/dist/runtime/delivery.js +7 -1
  43. package/dist/runtime/dingtalk.d.ts +6 -0
  44. package/dist/runtime/dingtalk.js +104 -7
  45. package/dist/runtime/events.js +5 -0
  46. package/dist/settings.d.ts +35 -1
  47. package/dist/settings.js +55 -1
  48. package/dist/shared/atomic-file.d.ts +2 -0
  49. package/dist/shared/atomic-file.js +17 -0
  50. package/dist/shared/serial-queue.d.ts +4 -0
  51. package/dist/shared/serial-queue.js +17 -0
  52. package/dist/tools/config.d.ts +10 -0
  53. package/dist/tools/config.js +28 -0
  54. package/dist/tools/index.d.ts +2 -1
  55. package/dist/tools/index.js +32 -0
  56. package/dist/tools/session-search.d.ts +17 -0
  57. package/dist/tools/session-search.js +56 -0
  58. package/dist/tools/skill-list.d.ts +17 -0
  59. package/dist/tools/skill-list.js +86 -0
  60. package/dist/tools/skill-manage.d.ts +34 -0
  61. package/dist/tools/skill-manage.js +138 -0
  62. package/dist/tools/skill-security.d.ts +10 -0
  63. package/dist/tools/skill-security.js +111 -0
  64. package/dist/tools/skill-view.d.ts +12 -0
  65. package/dist/tools/skill-view.js +43 -0
  66. package/package.json +3 -6
@@ -0,0 +1,34 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ declare const skillManageSchema: import("@sinclair/typebox").TObject<{
3
+ label: import("@sinclair/typebox").TString;
4
+ action: import("@sinclair/typebox").TString;
5
+ name: import("@sinclair/typebox").TString;
6
+ content: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
7
+ filePath: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
8
+ find: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
9
+ replace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
10
+ }>;
11
+ export type SkillManageAction = "create" | "patch" | "write_file";
12
+ export interface SkillManageResult {
13
+ action: SkillManageAction;
14
+ name: string;
15
+ path: string;
16
+ bytesWritten: number;
17
+ requiresResourceRefresh: boolean;
18
+ notice: string;
19
+ }
20
+ export interface SkillManageRequest {
21
+ action: SkillManageAction;
22
+ name: string;
23
+ content?: string;
24
+ filePath?: string;
25
+ find?: string;
26
+ replace?: string;
27
+ }
28
+ export interface SkillManageToolOptions {
29
+ workspaceDir: string;
30
+ workspacePath: string;
31
+ }
32
+ export declare function manageWorkspaceSkill(options: SkillManageToolOptions, request: SkillManageRequest): Promise<SkillManageResult>;
33
+ export declare function createSkillManageTool(options: SkillManageToolOptions): AgentTool<typeof skillManageSchema>;
34
+ export {};
@@ -0,0 +1,138 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { Type } from "@sinclair/typebox";
5
+ import { createAtomicTempPath, writeFileAtomically } from "../shared/atomic-file.js";
6
+ import { resolveSkillPath, resolveSkillSupportingFile, scanSkillContent, validateSkillMarkdown, } from "./skill-security.js";
7
+ const skillManageSchema = Type.Object({
8
+ label: Type.String({ description: "Brief description of the skill management change (shown to user)" }),
9
+ action: Type.String({ description: 'Supported actions: "create", "patch", or "write_file".' }),
10
+ name: Type.String({ description: "Workspace skill name" }),
11
+ content: Type.Optional(Type.String({ description: "Full content for create/write_file." })),
12
+ filePath: Type.Optional(Type.String({
13
+ description: "Optional supporting file path for patch/write_file. Defaults to SKILL.md for patch. Supporting files must be under references/, templates/, scripts/, or assets/.",
14
+ })),
15
+ find: Type.Optional(Type.String({ description: "Exact text to replace when action is patch." })),
16
+ replace: Type.Optional(Type.String({ description: "Replacement text when action is patch." })),
17
+ });
18
+ function toWorkspacePath(options, hostPath) {
19
+ if (hostPath.startsWith(options.workspaceDir)) {
20
+ return `${options.workspacePath}${hostPath.slice(options.workspaceDir.length)}`;
21
+ }
22
+ return hostPath;
23
+ }
24
+ function parseAction(action) {
25
+ if (action === "create" || action === "patch" || action === "write_file") {
26
+ return action;
27
+ }
28
+ throw new Error('Unsupported skill action. Use "create", "patch", or "write_file".');
29
+ }
30
+ function ensureSkillMarkdownSafe(content, name) {
31
+ const validation = validateSkillMarkdown(content, name);
32
+ if (!validation.ok) {
33
+ throw new Error(validation.error);
34
+ }
35
+ }
36
+ function ensureSupportingFileSafe(content) {
37
+ const validation = scanSkillContent(content);
38
+ if (!validation.ok) {
39
+ throw new Error(validation.error);
40
+ }
41
+ }
42
+ function applyUniquePatch(content, find, replace) {
43
+ if (!find) {
44
+ throw new Error("Patch requires a non-empty find string.");
45
+ }
46
+ const first = content.indexOf(find);
47
+ if (first < 0) {
48
+ throw new Error("Patch find string was not found.");
49
+ }
50
+ if (content.indexOf(find, first + find.length) >= 0) {
51
+ throw new Error("Patch find string matched multiple locations.");
52
+ }
53
+ return `${content.slice(0, first)}${replace}${content.slice(first + find.length)}`;
54
+ }
55
+ export async function manageWorkspaceSkill(options, request) {
56
+ const skillDir = resolveSkillPath(options.workspaceDir, request.name);
57
+ const skillPath = join(skillDir, "SKILL.md");
58
+ if (request.action === "create") {
59
+ if (existsSync(skillPath)) {
60
+ throw new Error(`Workspace skill "${request.name}" already exists.`);
61
+ }
62
+ const content = request.content ?? "";
63
+ ensureSkillMarkdownSafe(content, request.name);
64
+ await writeFileAtomically(skillPath, content);
65
+ return {
66
+ action: "create",
67
+ name: request.name,
68
+ path: toWorkspacePath(options, skillPath),
69
+ bytesWritten: Buffer.byteLength(content, "utf-8"),
70
+ requiresResourceRefresh: true,
71
+ notice: `已沉淀:创建 workspace skill \`${request.name}\`。`,
72
+ };
73
+ }
74
+ if (!existsSync(skillPath)) {
75
+ throw new Error(`Workspace skill "${request.name}" does not exist.`);
76
+ }
77
+ if (request.action === "write_file") {
78
+ if (!request.filePath) {
79
+ throw new Error("write_file requires filePath.");
80
+ }
81
+ const content = request.content ?? "";
82
+ const targetPath = resolveSkillSupportingFile(skillDir, request.filePath);
83
+ ensureSupportingFileSafe(content);
84
+ await writeFileAtomically(targetPath, content);
85
+ return {
86
+ action: "write_file",
87
+ name: request.name,
88
+ path: toWorkspacePath(options, targetPath),
89
+ bytesWritten: Buffer.byteLength(content, "utf-8"),
90
+ requiresResourceRefresh: true,
91
+ notice: `已沉淀:更新 workspace skill \`${request.name}\` 的支持文件。`,
92
+ };
93
+ }
94
+ const targetPath = request.filePath ? resolveSkillSupportingFile(skillDir, request.filePath) : skillPath;
95
+ const original = await readFile(targetPath, "utf-8");
96
+ const nextContent = applyUniquePatch(original, request.find ?? "", request.replace ?? "");
97
+ if (targetPath === skillPath) {
98
+ ensureSkillMarkdownSafe(nextContent, request.name);
99
+ }
100
+ else {
101
+ ensureSupportingFileSafe(nextContent);
102
+ }
103
+ const tempPath = createAtomicTempPath(targetPath);
104
+ await writeFileAtomically(targetPath, nextContent, tempPath);
105
+ return {
106
+ action: "patch",
107
+ name: request.name,
108
+ path: toWorkspacePath(options, targetPath),
109
+ bytesWritten: Buffer.byteLength(nextContent, "utf-8"),
110
+ requiresResourceRefresh: true,
111
+ notice: `已沉淀:更新 workspace skill \`${request.name}\`。`,
112
+ };
113
+ }
114
+ export function createSkillManageTool(options) {
115
+ return {
116
+ name: "skill_manage",
117
+ label: "skill_manage",
118
+ description: "Create or update workspace-level Pipiclaw skills as procedural memory. Supports create, patch, and write_file only; no channel-scoped skills.",
119
+ parameters: skillManageSchema,
120
+ execute: async (_toolCallId, args) => {
121
+ const result = await manageWorkspaceSkill(options, {
122
+ action: parseAction(args.action),
123
+ name: args.name,
124
+ content: args.content,
125
+ filePath: args.filePath,
126
+ find: args.find,
127
+ replace: args.replace,
128
+ });
129
+ return {
130
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
131
+ details: {
132
+ kind: "skill_manage",
133
+ ...result,
134
+ },
135
+ };
136
+ },
137
+ };
138
+ }
@@ -0,0 +1,10 @@
1
+ export interface SkillValidationResult {
2
+ ok: boolean;
3
+ error?: string;
4
+ }
5
+ export declare function validateSkillName(name: string): SkillValidationResult;
6
+ export declare function validateSkillFrontmatter(content: string, expectedName: string): SkillValidationResult;
7
+ export declare function resolveSkillPath(workspaceDir: string, name: string): string;
8
+ export declare function resolveSkillSupportingFile(skillDir: string, filePath: string): string;
9
+ export declare function scanSkillContent(content: string): SkillValidationResult;
10
+ export declare function validateSkillMarkdown(content: string, expectedName: string): SkillValidationResult;
@@ -0,0 +1,111 @@
1
+ import { resolve } from "node:path";
2
+ const SKILL_NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3
+ const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
4
+ const ALLOWED_SUPPORTING_DIRS = new Set(["references", "templates", "scripts", "assets"]);
5
+ const BLOCKED_CONTENT_PATTERNS = [
6
+ { pattern: /ignore\s+(all\s+)?(previous|prior)\s+instructions/i, message: "contains prompt-injection wording" },
7
+ {
8
+ pattern: /disregard\s+(all\s+)?(previous|prior|above)\s+(instructions|rules)/i,
9
+ message: "contains prompt-injection wording",
10
+ },
11
+ { pattern: /you\s+are\s+now\s+(a|an|the)\s+/i, message: "contains prompt-injection wording" },
12
+ { pattern: /new\s+system\s+prompt/i, message: "contains prompt-injection wording" },
13
+ { pattern: /exfiltrat(e|ion)|steal\s+(secrets?|credentials?|tokens?)/i, message: "contains exfiltration wording" },
14
+ { pattern: /rm\s+-rf\s+\/(?:\s|$)/i, message: "contains destructive root removal command" },
15
+ { pattern: /curl\b[\s\S]{0,120}\|\s*(?:sh|bash)\b/i, message: "contains pipe-to-shell install command" },
16
+ { pattern: /wget\b[\s\S]{0,120}\|\s*(?:sh|bash)\b/i, message: "contains pipe-to-shell install command" },
17
+ {
18
+ pattern: /cat\s+.*(?:\.env|id_rsa|id_ed25519|credentials|\.ssh\/|\.aws\/)/i,
19
+ message: "contains credential file access",
20
+ },
21
+ { pattern: /chmod\s+(?:777|[+]s)\b/i, message: "contains dangerous permission change" },
22
+ { pattern: /dd\s+if=.*of=\/dev\//i, message: "contains raw device write command" },
23
+ { pattern: /\b(?:mkfs|fdisk)\b/i, message: "contains disk formatting command" },
24
+ { pattern: /[\u200B-\u200D\uFEFF]/u, message: "contains invisible unicode characters" },
25
+ ];
26
+ function ok() {
27
+ return { ok: true };
28
+ }
29
+ function fail(error) {
30
+ return { ok: false, error };
31
+ }
32
+ function parseFrontmatter(content) {
33
+ const match = content.replace(/\r\n/g, "\n").match(FRONTMATTER_REGEX);
34
+ if (!match) {
35
+ return null;
36
+ }
37
+ const data = {};
38
+ for (const line of (match[1] ?? "").split("\n")) {
39
+ const fieldMatch = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
40
+ if (!fieldMatch) {
41
+ continue;
42
+ }
43
+ data[fieldMatch[1]] = fieldMatch[2].replace(/^["']|["']$/g, "").trim();
44
+ }
45
+ return { data, body: match[2] ?? "" };
46
+ }
47
+ export function validateSkillName(name) {
48
+ if (!SKILL_NAME_REGEX.test(name)) {
49
+ return fail("Skill name must match [a-z0-9]+(-[a-z0-9]+)*.");
50
+ }
51
+ return ok();
52
+ }
53
+ export function validateSkillFrontmatter(content, expectedName) {
54
+ const parsed = parseFrontmatter(content);
55
+ if (!parsed) {
56
+ return fail("SKILL.md must start with YAML frontmatter.");
57
+ }
58
+ if (parsed.data.name !== expectedName) {
59
+ return fail(`Skill frontmatter name must be "${expectedName}".`);
60
+ }
61
+ if (!parsed.data.description) {
62
+ return fail("Skill frontmatter must include a non-empty description.");
63
+ }
64
+ if (!parsed.body.trim()) {
65
+ return fail("Skill body must be non-empty.");
66
+ }
67
+ return ok();
68
+ }
69
+ export function resolveSkillPath(workspaceDir, name) {
70
+ const result = validateSkillName(name);
71
+ if (!result.ok) {
72
+ throw new Error(result.error);
73
+ }
74
+ const skillsDir = resolve(workspaceDir, "skills");
75
+ const skillDir = resolve(skillsDir, name);
76
+ if (skillDir !== resolve(skillsDir, name) || !skillDir.startsWith(`${skillsDir}/`)) {
77
+ throw new Error("Resolved skill path escaped workspace skills directory.");
78
+ }
79
+ return skillDir;
80
+ }
81
+ export function resolveSkillSupportingFile(skillDir, filePath) {
82
+ const normalized = filePath.replace(/\\/g, "/").replace(/^\/+/, "");
83
+ if (!normalized || normalized.includes("..")) {
84
+ throw new Error("Supporting file path must not be empty or contain '..'.");
85
+ }
86
+ const [topLevel] = normalized.split("/");
87
+ if (!topLevel || !ALLOWED_SUPPORTING_DIRS.has(topLevel)) {
88
+ throw new Error("Supporting files must live under references/, templates/, scripts/, or assets/.");
89
+ }
90
+ const base = resolve(skillDir);
91
+ const resolved = resolve(base, normalized);
92
+ if (!resolved.startsWith(`${base}/`)) {
93
+ throw new Error("Supporting file path escaped the skill directory.");
94
+ }
95
+ return resolved;
96
+ }
97
+ export function scanSkillContent(content) {
98
+ for (const { pattern, message } of BLOCKED_CONTENT_PATTERNS) {
99
+ if (pattern.test(content)) {
100
+ return fail(message);
101
+ }
102
+ }
103
+ return ok();
104
+ }
105
+ export function validateSkillMarkdown(content, expectedName) {
106
+ const frontmatter = validateSkillFrontmatter(content, expectedName);
107
+ if (!frontmatter.ok) {
108
+ return frontmatter;
109
+ }
110
+ return scanSkillContent(content);
111
+ }
@@ -0,0 +1,12 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ declare const skillViewSchema: import("@sinclair/typebox").TObject<{
3
+ label: import("@sinclair/typebox").TString;
4
+ name: import("@sinclair/typebox").TString;
5
+ filePath: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
6
+ }>;
7
+ export interface SkillViewToolOptions {
8
+ workspaceDir: string;
9
+ workspacePath: string;
10
+ }
11
+ export declare function createSkillViewTool(options: SkillViewToolOptions): AgentTool<typeof skillViewSchema>;
12
+ export {};
@@ -0,0 +1,43 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { resolveSkillPath, resolveSkillSupportingFile } from "./skill-security.js";
5
+ const skillViewSchema = Type.Object({
6
+ label: Type.String({ description: "Brief description of why you're viewing this skill (shown to user)" }),
7
+ name: Type.String({ description: "Workspace skill name" }),
8
+ filePath: Type.Optional(Type.String({
9
+ description: "Optional file path inside the skill directory. Defaults to SKILL.md. Supporting files must be under references/, templates/, scripts/, or assets/.",
10
+ })),
11
+ });
12
+ function toWorkspacePath(options, hostPath) {
13
+ if (hostPath.startsWith(options.workspaceDir)) {
14
+ return `${options.workspacePath}${hostPath.slice(options.workspaceDir.length)}`;
15
+ }
16
+ return hostPath;
17
+ }
18
+ export function createSkillViewTool(options) {
19
+ return {
20
+ name: "skill_view",
21
+ label: "skill_view",
22
+ description: "View a workspace-level skill SKILL.md file or an allowed supporting file.",
23
+ parameters: skillViewSchema,
24
+ execute: async (_toolCallId, { name, filePath }) => {
25
+ const skillDir = resolveSkillPath(options.workspaceDir, name);
26
+ const targetPath = filePath ? resolveSkillSupportingFile(skillDir, filePath) : join(skillDir, "SKILL.md");
27
+ const content = await readFile(targetPath, "utf-8");
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: JSON.stringify({
33
+ name,
34
+ path: toWorkspacePath(options, targetPath),
35
+ content,
36
+ }, null, 2),
37
+ },
38
+ ],
39
+ details: { kind: "skill_view", name, path: toWorkspacePath(options, targetPath) },
40
+ };
41
+ },
42
+ };
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oyasmi/pipiclaw",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "An AI assistant runtime for coding and team workflows, with DingTalk AI Cards, sub-agents, memory, and scheduled events.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,8 +21,8 @@
21
21
  "LICENSE"
22
22
  ],
23
23
  "scripts": {
24
- "clean": "shx rm -rf dist coverage",
25
- "build": "tsc -p tsconfig.build.json && shx chmod +x dist/main.js",
24
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('coverage', { recursive: true, force: true });\"",
25
+ "build": "tsc -p tsconfig.build.json && node -e \"require('node:fs').chmodSync('dist/main.js', 0o755)\"",
26
26
  "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
27
27
  "lint": "biome check .",
28
28
  "typecheck": "tsc --noEmit -p tsconfig.json",
@@ -39,7 +39,6 @@
39
39
  "@mozilla/readability": "^0.6.0",
40
40
  "@sinclair/typebox": "^0.34.0",
41
41
  "axios": "^1.7.0",
42
- "chalk": "^5.6.2",
43
42
  "croner": "^9.1.0",
44
43
  "diff": "^8.0.2",
45
44
  "dingtalk-stream": "^2.1.4",
@@ -51,11 +50,9 @@
51
50
  },
52
51
  "devDependencies": {
53
52
  "@biomejs/biome": "2.3.5",
54
- "@types/diff": "^7.0.2",
55
53
  "@types/jsdom": "^28.0.1",
56
54
  "@types/node": "^24.3.0",
57
55
  "@vitest/coverage-v8": "^3.2.4",
58
- "shx": "^0.4.0",
59
56
  "typescript": "^5.7.3",
60
57
  "vitest": "^3.2.4"
61
58
  },