pi-harness-runtime 1.1.95 → 1.1.97

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 (226) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/harness/loop-runtime.ts +3 -0
  3. package/index.ts +130 -29
  4. package/package.json +2 -1
  5. package/packages/a2a-adapter/package.json +1 -1
  6. package/packages/architecture-generator/package.json +1 -1
  7. package/packages/auth/package.json +1 -1
  8. package/packages/autonomous-refactor/package.json +1 -1
  9. package/packages/autonomous-runtime/package.json +1 -1
  10. package/packages/cache-strategy/package.json +1 -1
  11. package/packages/capability-registry/package.json +1 -1
  12. package/packages/checkpoint/package.json +1 -1
  13. package/packages/cli-plugin-sdk/package.json +1 -1
  14. package/packages/clipboard/package.json +1 -1
  15. package/packages/clipboard-plugin/package.json +1 -1
  16. package/packages/code-generation/package.json +1 -1
  17. package/packages/code-review/package.json +1 -1
  18. package/packages/codex-adapter/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/cookie-sanitizer/package.json +1 -1
  24. package/packages/cost-optimizer/package.json +1 -1
  25. package/packages/dependency-analyzer/package.json +1 -1
  26. package/packages/django-plugin/package.json +1 -1
  27. package/packages/doc-generator/package.json +1 -1
  28. package/packages/evaluation-engine/package.json +1 -1
  29. package/packages/evaluation-runner/package.json +1 -1
  30. package/packages/event-bus/package.json +1 -1
  31. package/packages/event-store/package.json +1 -1
  32. package/packages/experience-replay/package.json +1 -1
  33. package/packages/feedback-collector/package.json +1 -1
  34. package/packages/file-copy-helper/package.json +1 -1
  35. package/packages/framework-detector/package.json +1 -1
  36. package/packages/framework-plugin-sdk/package.json +1 -1
  37. package/packages/frappe-plugin/package.json +1 -1
  38. package/packages/generic-web-plugin/package.json +1 -1
  39. package/packages/health-monitor/package.json +1 -1
  40. package/packages/intent-analyzer/package.json +1 -1
  41. package/packages/jev-judge/package.json +1 -1
  42. package/packages/knowledge-graph/package.json +1 -1
  43. package/packages/knowledge-retrieval/package.json +1 -1
  44. package/packages/laravel-plugin/package.json +1 -1
  45. package/packages/learning-engine/package.json +1 -1
  46. package/packages/mcp-adapter/package.json +1 -1
  47. package/packages/memory-engine/package.json +1 -1
  48. package/packages/milestone-manager/package.json +1 -1
  49. package/packages/model-registry/package.json +1 -1
  50. package/packages/nextjs-plugin/package.json +1 -1
  51. package/packages/notification/package.json +1 -1
  52. package/packages/observability/package.json +1 -1
  53. package/packages/okf-indexer/package.json +1 -1
  54. package/packages/performance-optimizer/package.json +1 -1
  55. package/packages/privilege-broker/package.json +1 -1
  56. package/packages/project-analyzer/package.json +1 -1
  57. package/packages/project-bootstrap/package.json +1 -1
  58. package/packages/projection-engine/package.json +1 -1
  59. package/packages/prompt-compiler/package.json +1 -1
  60. package/packages/prompt-versioning/package.json +1 -1
  61. package/packages/provider-adapter-sdk/package.json +1 -1
  62. package/packages/provider-router/package.json +1 -1
  63. package/packages/provider-selector/package.json +1 -1
  64. package/packages/providers/package.json +1 -1
  65. package/packages/qdrant-skills/dist/client.d.ts +51 -0
  66. package/packages/qdrant-skills/dist/client.d.ts.map +1 -0
  67. package/packages/qdrant-skills/dist/client.js +105 -0
  68. package/packages/qdrant-skills/dist/client.js.map +1 -0
  69. package/packages/qdrant-skills/dist/embedder.d.ts +39 -0
  70. package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -0
  71. package/packages/qdrant-skills/dist/embedder.js +101 -0
  72. package/packages/qdrant-skills/dist/embedder.js.map +1 -0
  73. package/packages/qdrant-skills/dist/index.d.ts +25 -0
  74. package/packages/qdrant-skills/dist/index.d.ts.map +1 -0
  75. package/packages/qdrant-skills/dist/index.js +25 -0
  76. package/packages/qdrant-skills/dist/index.js.map +1 -0
  77. package/packages/qdrant-skills/package.json +27 -0
  78. package/packages/qdrant-skills/src/client.ts +178 -0
  79. package/packages/qdrant-skills/src/embedder.ts +137 -0
  80. package/packages/qdrant-skills/src/index.ts +42 -0
  81. package/packages/quota-manager/package.json +1 -1
  82. package/packages/rate-limiter/package.json +1 -1
  83. package/packages/react-vite-plugin/package.json +1 -1
  84. package/packages/release-manager/package.json +1 -1
  85. package/packages/requirement-compiler/package.json +1 -1
  86. package/packages/runtime/package.json +1 -1
  87. package/packages/scheduler/package.json +1 -1
  88. package/packages/scheduler-adapter/package.json +1 -1
  89. package/packages/session/package.json +1 -1
  90. package/packages/session-api/package.json +1 -1
  91. package/packages/session-export/package.json +1 -1
  92. package/packages/shared-context/package.json +1 -1
  93. package/packages/skill-mcp-client/package.json +1 -1
  94. package/packages/skill-registry/package.json +1 -1
  95. package/packages/skills/dist/index.d.ts +40 -0
  96. package/packages/skills/dist/index.d.ts.map +1 -0
  97. package/packages/skills/dist/index.js +84 -0
  98. package/packages/skills/dist/index.js.map +1 -0
  99. package/packages/skills/dist/linter.d.ts +38 -0
  100. package/packages/skills/dist/linter.d.ts.map +1 -0
  101. package/packages/skills/dist/linter.js +323 -0
  102. package/packages/skills/dist/linter.js.map +1 -0
  103. package/packages/skills/dist/loader.d.ts +74 -0
  104. package/packages/skills/dist/loader.d.ts.map +1 -0
  105. package/packages/skills/dist/loader.js +280 -0
  106. package/packages/skills/dist/loader.js.map +1 -0
  107. package/packages/skills/dist/parser.d.ts +32 -0
  108. package/packages/skills/dist/parser.d.ts.map +1 -0
  109. package/packages/skills/dist/parser.js +459 -0
  110. package/packages/skills/dist/parser.js.map +1 -0
  111. package/packages/skills/dist/scanner.d.ts +33 -0
  112. package/packages/skills/dist/scanner.d.ts.map +1 -0
  113. package/packages/skills/dist/scanner.js +257 -0
  114. package/packages/skills/dist/scanner.js.map +1 -0
  115. package/packages/skills/dist/skill-commands.d.ts +33 -0
  116. package/packages/skills/dist/skill-commands.d.ts.map +1 -0
  117. package/packages/skills/dist/skill-commands.js +239 -0
  118. package/packages/skills/dist/skill-commands.js.map +1 -0
  119. package/packages/skills/dist/skill-tool.d.ts +120 -0
  120. package/packages/skills/dist/skill-tool.d.ts.map +1 -0
  121. package/packages/skills/dist/skill-tool.js +239 -0
  122. package/packages/skills/dist/skill-tool.js.map +1 -0
  123. package/packages/skills/dist/skill-versioning.d.ts +123 -0
  124. package/packages/skills/dist/skill-versioning.d.ts.map +1 -0
  125. package/packages/skills/dist/skill-versioning.js +240 -0
  126. package/packages/skills/dist/skill-versioning.js.map +1 -0
  127. package/packages/skills/dist/types.d.ts +226 -0
  128. package/packages/skills/dist/types.d.ts.map +1 -0
  129. package/packages/skills/dist/types.js +21 -0
  130. package/packages/skills/dist/types.js.map +1 -0
  131. package/packages/skills/package.json +46 -0
  132. package/packages/skills/src/index.ts +95 -0
  133. package/packages/skills/src/linter.ts +371 -0
  134. package/packages/skills/src/loader.ts +335 -0
  135. package/packages/skills/src/parser.ts +548 -0
  136. package/packages/skills/src/scanner.ts +322 -0
  137. package/packages/skills/src/skill-commands.ts +318 -0
  138. package/packages/skills/src/skill-tool.ts +324 -0
  139. package/packages/skills/src/skill-versioning.ts +345 -0
  140. package/packages/skills/src/types.ts +259 -0
  141. package/packages/skills/test/init-skills.test.ts +118 -0
  142. package/packages/sprint-planner/package.json +1 -1
  143. package/packages/subscription-engine/package.json +1 -1
  144. package/packages/task-compiler/package.json +1 -1
  145. package/packages/tencentdb-memory/package.json +1 -1
  146. package/packages/tencentdb-sync/package.json +1 -1
  147. package/packages/test-data-generator/package.json +1 -1
  148. package/packages/test-generator/package.json +1 -1
  149. package/packages/todo-bd-sync/package.json +1 -1
  150. package/packages/token-estimation/package.json +1 -1
  151. package/packages/token-optimizer/package.json +1 -1
  152. package/packages/trajectory/dist/src/curator.d.ts +189 -0
  153. package/packages/trajectory/dist/src/curator.d.ts.map +1 -0
  154. package/packages/trajectory/dist/src/curator.js +340 -0
  155. package/packages/trajectory/dist/src/curator.js.map +1 -0
  156. package/packages/trajectory/dist/src/journey-renderer.d.ts +53 -0
  157. package/packages/trajectory/dist/src/journey-renderer.d.ts.map +1 -0
  158. package/packages/trajectory/dist/src/journey-renderer.js +324 -0
  159. package/packages/trajectory/dist/src/journey-renderer.js.map +1 -0
  160. package/packages/trajectory/dist/src/learning-graph.d.ts +159 -0
  161. package/packages/trajectory/dist/src/learning-graph.d.ts.map +1 -0
  162. package/packages/trajectory/dist/src/learning-graph.js +258 -0
  163. package/packages/trajectory/dist/src/learning-graph.js.map +1 -0
  164. package/packages/trajectory/dist/src/memory-store.d.ts +146 -0
  165. package/packages/trajectory/dist/src/memory-store.d.ts.map +1 -0
  166. package/packages/trajectory/dist/src/memory-store.js +472 -0
  167. package/packages/trajectory/dist/src/memory-store.js.map +1 -0
  168. package/packages/trajectory/dist/src/review-fork.d.ts +164 -0
  169. package/packages/trajectory/dist/src/review-fork.d.ts.map +1 -0
  170. package/packages/trajectory/dist/src/review-fork.js +332 -0
  171. package/packages/trajectory/dist/src/review-fork.js.map +1 -0
  172. package/packages/trajectory/dist/src/write-approval.d.ts +136 -0
  173. package/packages/trajectory/dist/src/write-approval.d.ts.map +1 -0
  174. package/packages/trajectory/dist/src/write-approval.js +281 -0
  175. package/packages/trajectory/dist/src/write-approval.js.map +1 -0
  176. package/packages/trajectory/package.json +1 -1
  177. package/packages/trajectory/src/curator.ts +485 -0
  178. package/packages/trajectory/src/journey-renderer.ts +399 -0
  179. package/packages/trajectory/src/learning-graph.ts +363 -0
  180. package/packages/trajectory/src/memory-store.ts +578 -0
  181. package/packages/trajectory/src/review-fork.ts +450 -0
  182. package/packages/trajectory/src/write-approval.ts +372 -0
  183. package/packages/tui/package.json +1 -1
  184. package/packages/types/package.json +1 -1
  185. package/packages/workflow-events/package.json +1 -1
  186. package/packages/workspace-scanner/package.json +1 -1
  187. package/packages/worktree/package.json +1 -1
  188. package/packages/write-review/dist/blackboard.d.ts +98 -0
  189. package/packages/write-review/dist/blackboard.d.ts.map +1 -0
  190. package/packages/write-review/dist/blackboard.js +289 -0
  191. package/packages/write-review/dist/blackboard.js.map +1 -0
  192. package/packages/write-review/dist/event-bus.d.ts +106 -0
  193. package/packages/write-review/dist/event-bus.d.ts.map +1 -0
  194. package/packages/write-review/dist/event-bus.js +294 -0
  195. package/packages/write-review/dist/event-bus.js.map +1 -0
  196. package/packages/write-review/dist/gate.d.ts +37 -0
  197. package/packages/write-review/dist/gate.d.ts.map +1 -0
  198. package/packages/write-review/dist/gate.js +138 -0
  199. package/packages/write-review/dist/gate.js.map +1 -0
  200. package/packages/write-review/dist/index.d.ts +22 -0
  201. package/packages/write-review/dist/index.d.ts.map +1 -0
  202. package/packages/write-review/dist/index.js +60 -0
  203. package/packages/write-review/dist/index.js.map +1 -0
  204. package/packages/write-review/dist/injection.d.ts +40 -0
  205. package/packages/write-review/dist/injection.d.ts.map +1 -0
  206. package/packages/write-review/dist/injection.js +179 -0
  207. package/packages/write-review/dist/injection.js.map +1 -0
  208. package/packages/write-review/dist/review.d.ts +54 -0
  209. package/packages/write-review/dist/review.d.ts.map +1 -0
  210. package/packages/write-review/dist/review.js +154 -0
  211. package/packages/write-review/dist/review.js.map +1 -0
  212. package/packages/write-review/dist/trigger.d.ts +39 -0
  213. package/packages/write-review/dist/trigger.d.ts.map +1 -0
  214. package/packages/write-review/dist/trigger.js +107 -0
  215. package/packages/write-review/dist/trigger.js.map +1 -0
  216. package/packages/write-review/dist/types.d.ts +78 -0
  217. package/packages/write-review/dist/types.d.ts.map +1 -0
  218. package/packages/write-review/dist/types.js +26 -0
  219. package/packages/write-review/dist/types.js.map +1 -0
  220. package/packages/write-review/package.json +1 -1
  221. package/packages/write-review/src/event-bus.ts +351 -0
  222. package/packages/write-review/src/index.ts +1 -0
  223. package/packages/write-review/src/injection.ts +25 -3
  224. package/proactive-compact.ts +51 -7
  225. package/scripts/sync-skills.ts +275 -0
  226. package/scripts/sync-to-qdrant.ts +398 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Skills Sync Script
3
+ *
4
+ * Sync skills from a source directory to ~/.pi-harness-runtime/skills/
5
+ *
6
+ * Usage:
7
+ * bun scripts/sync-skills.ts --from ~/my-skills --to ~/.pi-harness-runtime/skills
8
+ * bun scripts/sync-skills.ts --from ~/frappe-bench/.claude-plugins/moocoding-skills/skills
9
+ * bun scripts/sync-skills.ts --list # List skills in target dir
10
+ * bun scripts/sync-skills.ts --check-only # Check without copying
11
+ */
12
+
13
+ import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, rmSync } from "node:fs";
14
+ import { join, basename, dirname } from "node:path";
15
+ import { homedir } from "node:os";
16
+
17
+ // Default paths
18
+ const DEFAULT_TARGET = join(homedir(), ".pi-harness-runtime", "skills");
19
+
20
+ interface SyncOptions {
21
+ from: string;
22
+ to: string;
23
+ checkOnly: boolean;
24
+ verbose: boolean;
25
+ clean: boolean;
26
+ }
27
+
28
+ /**
29
+ * Parse command line arguments
30
+ */
31
+ function parseArgs(): SyncOptions {
32
+ const args = process.argv.slice(2);
33
+ let from: string | undefined;
34
+ let to = DEFAULT_TARGET;
35
+ let checkOnly = false;
36
+ let verbose = false;
37
+ let clean = false;
38
+
39
+ for (let i = 0; i < args.length; i++) {
40
+ const arg = args[i];
41
+ if (arg === "--from" || arg === "-f") {
42
+ from = args[++i];
43
+ } else if (arg === "--to" || arg === "-t") {
44
+ to = args[++i];
45
+ } else if (arg === "--check-only" || arg === "-c") {
46
+ checkOnly = true;
47
+ } else if (arg === "--verbose" || arg === "-v") {
48
+ verbose = true;
49
+ } else if (arg === "--clean") {
50
+ clean = true;
51
+ } else if (arg === "--help" || arg === "-h") {
52
+ printHelp();
53
+ process.exit(0);
54
+ } else if (arg === "--list") {
55
+ listSkills(to);
56
+ process.exit(0);
57
+ }
58
+ }
59
+
60
+ if (!from) {
61
+ console.error("Error: --from <source> is required");
62
+ printHelp();
63
+ process.exit(1);
64
+ }
65
+
66
+ return { from, to, checkOnly, verbose, clean };
67
+ }
68
+
69
+ /**
70
+ * Print help message
71
+ */
72
+ function printHelp(): void {
73
+ console.log(`
74
+ Skills Sync Script
75
+ ─────────────────
76
+ Sync skills from a source directory to ~/.pi-harness-runtime/skills/
77
+
78
+ Usage:
79
+ bun scripts/sync-skills.ts --from <source> [options]
80
+
81
+ Options:
82
+ --from, -f <path> Source directory containing skills (required)
83
+ --to, -t <path> Target directory (default: ~/.pi-harness-runtime/skills)
84
+ --list List skills in target directory
85
+ --check-only, -c Check what would be copied without copying
86
+ --verbose, -v Show detailed output
87
+ --clean Remove skills not in source
88
+ --help, -h Show this help message
89
+
90
+ Examples:
91
+ # Sync from a local directory
92
+ bun scripts/sync-skills.ts --from ~/my-skills
93
+
94
+ # Sync from frappe-bench moocoding skills
95
+ bun scripts/sync-skills.ts --from ~/frappe-bench/.claude-plugins/moocoding-skills/skills
96
+
97
+ # Check what would be synced
98
+ bun scripts/sync-skills.ts --from ~/my-skills --check-only
99
+
100
+ # List current skills
101
+ bun scripts/sync-skills.ts --list
102
+ `);
103
+ }
104
+
105
+ /**
106
+ * List skills in a directory
107
+ */
108
+ function listSkills(dir: string): void {
109
+ if (!existsSync(dir)) {
110
+ console.log(`Directory does not exist: ${dir}`);
111
+ console.log(`Run without --list to create it.`);
112
+ return;
113
+ }
114
+
115
+ const skills = getSkillDirs(dir);
116
+ if (skills.length === 0) {
117
+ console.log(`No skills found in ${dir}`);
118
+ return;
119
+ }
120
+
121
+ console.log(`Skills in ${dir}:`);
122
+ console.log("");
123
+ for (const skill of skills) {
124
+ console.log(` - ${basename(skill)}`);
125
+ }
126
+ console.log("");
127
+ console.log(`Total: ${skills.length} skills`);
128
+ }
129
+
130
+ /**
131
+ * Get skill directories (directories containing SKILL.md)
132
+ */
133
+ function getSkillDirs(dir: string): string[] {
134
+ if (!existsSync(dir)) {
135
+ return [];
136
+ }
137
+
138
+ const skills: string[] = [];
139
+ const entries = readdirSync(dir);
140
+
141
+ for (const entry of entries) {
142
+ const fullPath = join(dir, entry);
143
+ const stat = statSync(fullPath);
144
+
145
+ if (stat.isDirectory()) {
146
+ // Check if it contains SKILL.md
147
+ const skillMdPath = join(fullPath, "SKILL.md");
148
+ if (existsSync(skillMdPath)) {
149
+ skills.push(fullPath);
150
+ } else {
151
+ // Recursively search subdirectories
152
+ skills.push(...getSkillDirs(fullPath));
153
+ }
154
+ }
155
+ }
156
+
157
+ return skills;
158
+ }
159
+
160
+ /**
161
+ * Copy a directory recursively
162
+ */
163
+ function copyDirRecursive(src: string, dest: string, verbose: boolean): void {
164
+ if (!existsSync(dest)) {
165
+ mkdirSync(dest, { recursive: true });
166
+ }
167
+
168
+ const entries = readdirSync(src);
169
+
170
+ for (const entry of entries) {
171
+ const srcPath = join(src, entry);
172
+ const destPath = join(dest, entry);
173
+ const stat = statSync(srcPath);
174
+
175
+ if (stat.isDirectory()) {
176
+ copyDirRecursive(srcPath, destPath, verbose);
177
+ } else {
178
+ copyFileSync(srcPath, destPath);
179
+ if (verbose) {
180
+ console.log(` Copied: ${entry}`);
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Sync skills from source to target
188
+ */
189
+ function syncSkills(options: SyncOptions): void {
190
+ const { from, to, checkOnly, verbose, clean } = options;
191
+
192
+ // Validate source
193
+ if (!existsSync(from)) {
194
+ console.error(`Error: Source directory does not exist: ${from}`);
195
+ process.exit(1);
196
+ }
197
+
198
+ // Create target if needed
199
+ if (!checkOnly && !existsSync(to)) {
200
+ console.log(`Creating target directory: ${to}`);
201
+ mkdirSync(to, { recursive: true });
202
+ }
203
+
204
+ // Find skills in source
205
+ console.log(`Scanning source: ${from}`);
206
+ const sourceSkills = getSkillDirs(from);
207
+ console.log(`Found ${sourceSkills.length} skill(s) in source`);
208
+
209
+ if (sourceSkills.length === 0) {
210
+ console.log("No skills found. Make sure source contains directories with SKILL.md files.");
211
+ return;
212
+ }
213
+
214
+ if (checkOnly) {
215
+ console.log("\n[CHECK ONLY] Would sync the following skills:");
216
+ } else {
217
+ console.log("\nSyncing skills:");
218
+ }
219
+
220
+ const targetSkills: string[] = [];
221
+ const toClean: string[] = [];
222
+
223
+ for (const skillPath of sourceSkills) {
224
+ const skillName = basename(skillPath);
225
+ const targetPath = join(to, skillName);
226
+
227
+ console.log(` ${checkOnly ? "[ ]" : "[x]"} ${skillName}`);
228
+ targetSkills.push(skillName);
229
+
230
+ if (!checkOnly) {
231
+ copyDirRecursive(skillPath, targetPath, verbose);
232
+ }
233
+ }
234
+
235
+ // Clean: remove skills in target not in source
236
+ if (clean) {
237
+ console.log("\nChecking for skills to remove...");
238
+ const existingTargetSkills = getSkillDirs(to);
239
+ const sourceSkillNames = new Set(sourceSkills.map(s => basename(s)));
240
+
241
+ for (const existingPath of existingTargetSkills) {
242
+ const existingName = basename(existingPath);
243
+ if (!sourceSkillNames.has(existingName)) {
244
+ toClean.push(existingPath);
245
+ console.log(` [-] ${existingName} (not in source)`);
246
+ }
247
+ }
248
+
249
+ if (!checkOnly && toClean.length > 0) {
250
+ console.log("\nRemoving orphaned skills...");
251
+ for (const path of toClean) {
252
+ rmSync(path, { recursive: true, force: true });
253
+ console.log(` Removed: ${basename(path)}`);
254
+ }
255
+ }
256
+ }
257
+
258
+ console.log("");
259
+ if (checkOnly) {
260
+ console.log(`Check complete. Run without --check-only to sync.`);
261
+ } else {
262
+ console.log(`Sync complete! ${sourceSkills.length} skill(s) synced to ${to}`);
263
+ }
264
+
265
+ // Show next steps
266
+ console.log("");
267
+ console.log("Next steps:");
268
+ console.log(` 1. Restart pi to load new skills`);
269
+ console.log(` 2. Or run /reload in pi`);
270
+ console.log(` 3. List skills: /skill list`);
271
+ }
272
+
273
+ // Main
274
+ const options = parseArgs();
275
+ syncSkills(options);
@@ -0,0 +1,398 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Sync Skills to Qdrant
4
+ *
5
+ * Uploads local skills to Qdrant vector database for team sharing.
6
+ * SANITIZES client-specific data by replacing with placeholders.
7
+ *
8
+ * Usage:
9
+ * bun scripts/sync-to-qdrant.ts --from ~/.pi-harness-runtime/skills
10
+ * bun scripts/sync-to-qdrant.ts --list # List skills in Qdrant
11
+ * bun scripts/sync-to-qdrant.ts --search "..." # Search skills in Qdrant
12
+ */
13
+
14
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
15
+ import { join, basename } from "node:path";
16
+ import { QdrantClient } from "@qdrant/js-client-rest";
17
+
18
+ // =============================================================================
19
+ // SANITIZATION: Replace client-specific names with placeholders
20
+ // =============================================================================
21
+
22
+ interface SanitizationRule {
23
+ pattern: RegExp;
24
+ replacement: string;
25
+ }
26
+
27
+ const SANITIZATION_RULES: SanitizationRule[] = [
28
+ // Client company names (case-insensitive)
29
+ { pattern: /\binpac\b/gi, replacement: "[Client A]" },
30
+ { pattern: /\bm-capital\b/gi, replacement: "[Client A]" },
31
+ { pattern: /\bmcapital\b/gi, replacement: "[Client A]" },
32
+ { pattern: /\bdigisoft\b/gi, replacement: "[Client B]" },
33
+
34
+ // Project/app specific names
35
+ { pattern: /\bgse-insurance\b/gi, replacement: "[Insurance Project]" },
36
+ { pattern: /\btbs-import\b/gi, replacement: "[Import Project]" },
37
+ { pattern: /\bdigisoft_erp\b/gi, replacement: "[Client B Erp]" },
38
+ { pattern: /\bm_capital\b/gi, replacement: "[Client A]" },
39
+ { pattern: /\binpac_\b/gi, replacement: "[Client A]_" },
40
+ { pattern: /\bpaperclip_\b/gi, replacement: "[Client D]_" },
41
+ { pattern: /\bopenclaw_\b/gi, replacement: "[Client F]_" },
42
+
43
+ // Generic replacements
44
+ { pattern: /Company Name/gi, replacement: "[Client Name]" },
45
+ { pattern: /your company/gi, replacement: "[Client]" },
46
+ { pattern: /our client/gi, replacement: "[Client]" },
47
+ { pattern: /client-specific/gi, replacement: "[Project-specific]" },
48
+ ];
49
+
50
+ // Skill name sanitization (for skill directory name -> sanitized display name)
51
+ const SKILL_NAME_MAP: Record<string, string> = {
52
+ 'gse-insurance-step5-slot-filter': 'insurance-project-step-filter',
53
+ 'inpac-pe-approval-workflow': 'client-a-pe-approval-workflow',
54
+ 'inpac-po-approval-workflow': 'client-a-po-approval-workflow',
55
+ 'inpac-pr-approval-workflow': 'client-a-pr-approval-workflow',
56
+ 'tbs-import-clearance-lcv': 'import-project-clearance',
57
+ 'whispertool': 'whisper-tool',
58
+ };
59
+
60
+ /**
61
+ * Sanitize text by replacing client-specific patterns
62
+ */
63
+ function sanitize(text: string): string {
64
+ let result = text;
65
+ for (const rule of SANITIZATION_RULES) {
66
+ result = result.replace(rule.pattern, rule.replacement);
67
+ }
68
+ return result;
69
+ }
70
+
71
+ // =============================================================================
72
+ // Qdrant Config
73
+ // =============================================================================
74
+
75
+ interface QdrantConfig {
76
+ url: string;
77
+ apiKey: string;
78
+ collection: string;
79
+ }
80
+
81
+ function getQdrantConfig(): QdrantConfig | null {
82
+ let apiKey = process.env.QDRANT_API_KEY;
83
+ const url = process.env.QDRANT_CLUSTER_ENDPOINT || "https://api.qdrant.tech";
84
+ const collection = process.env.QDRANT_COLLECTION || "pi-harness-skills";
85
+
86
+ if (!apiKey) {
87
+ const keyPath = `${process.env.HOME}/.pi-harness-runtime/keys/qdrant-api-key.txt`;
88
+ if (existsSync(keyPath)) {
89
+ apiKey = readFileSync(keyPath, "utf-8").trim();
90
+ }
91
+ }
92
+
93
+ if (!apiKey) return null;
94
+
95
+ return { url, apiKey, collection };
96
+ }
97
+
98
+ function parseHostPort(url: string): { host: string; port: number } {
99
+ const match = url.match(/https?:\/\/([^:]+)(?::(\d+))?/);
100
+ return {
101
+ host: match?.[1] || url,
102
+ port: parseInt(match?.[2] || "6333"),
103
+ };
104
+ }
105
+
106
+ // =============================================================================
107
+ // Skill Reading
108
+ // =============================================================================
109
+
110
+ interface SkillDoc {
111
+ id: number;
112
+ name: string;
113
+ description: string;
114
+ body: string;
115
+ text: string;
116
+ }
117
+
118
+ function parseFrontmatter(content: string): Record<string, string> | null {
119
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
120
+ if (!match) return null;
121
+
122
+ const fm: Record<string, string> = {};
123
+ for (const line of match[1].split("\n")) {
124
+ const idx = line.indexOf(":");
125
+ if (idx === -1) continue;
126
+ fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
127
+ }
128
+ return fm;
129
+ }
130
+
131
+ function readSkill(dirPath: string): SkillDoc | null {
132
+ const skillPath = join(dirPath, "SKILL.md");
133
+ if (!existsSync(skillPath)) return null;
134
+
135
+ try {
136
+ const rawContent = readFileSync(skillPath, "utf-8");
137
+ const fm = parseFrontmatter(rawContent);
138
+ if (!fm) return null;
139
+
140
+ // Get original skill name from directory
141
+ const originalName = fm.name || basename(dirPath);
142
+
143
+ // Sanitize content
144
+ const sanitizedContent = sanitize(rawContent);
145
+
146
+ // Use name map for known sensitive skill names, otherwise sanitize
147
+ const name = SKILL_NAME_MAP[originalName] || sanitize(originalName);
148
+
149
+ const description = sanitize(fm.description || "");
150
+ const body = sanitizedContent.replace(/^---[\s\S]*?---\n/, "").trim();
151
+
152
+ // Text for embedding (sanitized)
153
+ const text = `${name}. ${description}. ${body.slice(0, 1000)}`;
154
+
155
+ return {
156
+ id: 0,
157
+ name,
158
+ description,
159
+ body: body.slice(0, 2000),
160
+ text,
161
+ };
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ function findSkillDirs(dir: string): string[] {
168
+ if (!existsSync(dir)) return [];
169
+
170
+ return readdirSync(dir)
171
+ .map(entry => join(dir, entry))
172
+ .filter(fullPath => {
173
+ const stat = statSync(fullPath);
174
+ return stat.isDirectory() && existsSync(join(fullPath, "SKILL.md"));
175
+ });
176
+ }
177
+
178
+ // =============================================================================
179
+ // Qdrant Operations
180
+ // =============================================================================
181
+
182
+ async function ensureCollection(client: QdrantClient, name: string): Promise<void> {
183
+ try {
184
+ await client.createCollection(name, {
185
+ vectors: { size: 384, distance: "Cosine" }
186
+ });
187
+ } catch {
188
+ // Already exists - OK
189
+ }
190
+ }
191
+
192
+ async function clearCollection(client: QdrantClient, name: string): Promise<void> {
193
+ try {
194
+ await client.deleteCollection(name);
195
+ await ensureCollection(client, name);
196
+ } catch {
197
+ // Ignore
198
+ }
199
+ }
200
+
201
+ async function upsertSkills(
202
+ client: QdrantClient,
203
+ collectionName: string,
204
+ skills: SkillDoc[]
205
+ ): Promise<void> {
206
+ const points = skills.map((skill, idx) => ({
207
+ id: idx + 1,
208
+ payload: {
209
+ name: skill.name,
210
+ description: skill.description,
211
+ body: skill.body,
212
+ },
213
+ vector: {
214
+ text: skill.text,
215
+ model: "sentence-transformers/all-MiniLM-L6-v2"
216
+ }
217
+ }));
218
+
219
+ await client.upsert(collectionName, { wait: true, points });
220
+ }
221
+
222
+ async function searchSkills(
223
+ client: QdrantClient,
224
+ collectionName: string,
225
+ query: string
226
+ ): Promise<void> {
227
+ const results = await client.query(collectionName, {
228
+ query: {
229
+ text: query,
230
+ model: "sentence-transformers/all-MiniLM-L6-v2"
231
+ },
232
+ limit: 10,
233
+ with_payload: true,
234
+ } as Parameters<typeof client.query>[1]);
235
+
236
+ const points = (results as { points?: unknown[] })?.points || [];
237
+
238
+ console.error(`\nFound ${points.length} skills:\n`);
239
+ for (const r of points as Array<{ score: number; payload?: Record<string, string> }>) {
240
+ console.error(`[${(r.score * 100).toFixed(1)}%] ${r.payload?.name || "unknown"}`);
241
+ console.error(` ${(r.payload?.description || "").slice(0, 80)}...`);
242
+ console.error("");
243
+ }
244
+ }
245
+
246
+ async function getCollectionInfo(client: QdrantClient, name: string): Promise<number> {
247
+ try {
248
+ const info = await client.getCollection(name);
249
+ return (info as { result?: { points_count?: number } })?.result?.points_count || 0;
250
+ } catch {
251
+ return 0;
252
+ }
253
+ }
254
+
255
+ // =============================================================================
256
+ // CLI Commands
257
+ // =============================================================================
258
+
259
+ async function syncToQdrant(sourceDir: string, clear: boolean = false): Promise<void> {
260
+ const config = getQdrantConfig();
261
+ if (!config) {
262
+ console.error("Error: Qdrant API key not found.");
263
+ console.error("Set QDRANT_API_KEY or create:");
264
+ console.error(" ~/.pi-harness-runtime/keys/qdrant-api-key.txt");
265
+ process.exit(1);
266
+ }
267
+
268
+ const { host, port } = parseHostPort(config.url);
269
+ const client = new QdrantClient({ host, port, apiKey: config.apiKey });
270
+
271
+ console.error(`[Sync] Connecting to Qdrant: ${config.url}`);
272
+ console.error(`[Sync] Collection: ${config.collection}`);
273
+
274
+ if (clear) {
275
+ console.error("[Sync] Clearing collection...");
276
+ await clearCollection(client, config.collection);
277
+ } else {
278
+ await ensureCollection(client, config.collection);
279
+ }
280
+
281
+ console.error(`[Sync] Scanning: ${sourceDir}`);
282
+ const skillDirs = findSkillDirs(sourceDir);
283
+
284
+ // Read and sanitize ALL skills
285
+ const skills: SkillDoc[] = [];
286
+
287
+ for (const dir of skillDirs) {
288
+ const skill = readSkill(dir);
289
+ if (skill) {
290
+ skills.push(skill);
291
+ }
292
+ }
293
+
294
+ console.error(`[Sync] Found ${skillDirs.length} skills`);
295
+ console.error(`[Sync] All skills sanitized and ready to upload`);
296
+
297
+ if (skills.length === 0) {
298
+ console.error("[Sync] No skills to upload.");
299
+ return;
300
+ }
301
+
302
+ console.error(`[Sync] Uploading ${skills.length} sanitized skills to Qdrant...`);
303
+ await upsertSkills(client, config.collection, skills);
304
+ console.error(`[Sync] Complete! ${skills.length} sanitized skills uploaded.`);
305
+ }
306
+
307
+ async function listSkills(): Promise<void> {
308
+ const config = getQdrantConfig();
309
+ if (!config) {
310
+ console.error("Error: Qdrant API key not found.");
311
+ process.exit(1);
312
+ }
313
+
314
+ const { host, port } = parseHostPort(config.url);
315
+ const client = new QdrantClient({ host, port, apiKey: config.apiKey });
316
+
317
+ const count = await getCollectionInfo(client, config.collection);
318
+ console.error(`Collection: ${config.collection}`);
319
+ console.error(`Skills: ${count}`);
320
+ }
321
+
322
+ async function search(query: string): Promise<void> {
323
+ const config = getQdrantConfig();
324
+ if (!config) {
325
+ console.error("Error: Qdrant API key not found.");
326
+ process.exit(1);
327
+ }
328
+
329
+ const { host, port } = parseHostPort(config.url);
330
+ const client = new QdrantClient({ host, port, apiKey: config.apiKey });
331
+
332
+ console.error(`[Search] Query: "${query}"`);
333
+ await searchSkills(client, config.collection, query);
334
+ }
335
+
336
+ // =============================================================================
337
+ // Main
338
+ // =============================================================================
339
+
340
+ const args = process.argv.slice(2);
341
+
342
+ if (args.includes("--help")) {
343
+ console.log(`
344
+ Sync Skills to Qdrant (Sanitized)
345
+ ──────────────────────────────────
346
+ Upload skills to Qdrant for team sharing.
347
+ Client-specific names are REPLACED with placeholders.
348
+
349
+ Sanitization Rules:
350
+ inpac → [Client A]
351
+ m-capital → [Client A]
352
+ digisoft → [Client B]
353
+ cloudshot → [Client C]
354
+ paperclip → [Client D]
355
+ teamw → [Client E]
356
+ openclaw → [Client F]
357
+ autoresearch → [Internal Tool]
358
+
359
+ Usage:
360
+ bun scripts/sync-to-qdrant.ts --from <dir> Sync skills from directory
361
+ bun scripts/sync-to-qdrant.ts --from <dir> --clear Clear and re-upload
362
+ bun scripts/sync-to-qdrant.ts --list List skills in Qdrant
363
+ bun scripts/sync-to-qdrant.ts --search <query> Search skills in Qdrant
364
+ bun scripts/sync-to-qdrant.ts --help Show this help
365
+
366
+ Environment:
367
+ QDRANT_API_KEY Your Qdrant Cloud API key
368
+ QDRANT_CLUSTER_ENDPOINT Qdrant URL
369
+ QDRANT_COLLECTION Collection name (default: pi-harness-skills)
370
+
371
+ Or create file:
372
+ ~/.pi-harness-runtime/keys/qdrant-api-key.txt
373
+ `);
374
+ process.exit(0);
375
+ }
376
+
377
+ if (args.includes("--list")) {
378
+ await listSkills();
379
+ process.exit(0);
380
+ }
381
+
382
+ const searchIdx = args.indexOf("--search");
383
+ if (searchIdx !== -1 && args[searchIdx + 1]) {
384
+ await search(args[searchIdx + 1]);
385
+ process.exit(0);
386
+ }
387
+
388
+ const fromIdx = args.indexOf("--from");
389
+ if (fromIdx === -1 || !args[fromIdx + 1]) {
390
+ console.error("Error: --from <directory> is required");
391
+ console.error("Use --help for usage information.");
392
+ process.exit(1);
393
+ }
394
+
395
+ const sourceDir = args[fromIdx + 1];
396
+ const clear = args.includes("--clear");
397
+
398
+ await syncToQdrant(sourceDir, clear);