total-recall-brain 3.9.0 → 3.11.0

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 (49) hide show
  1. package/bin/antigravity.mjs +62 -183
  2. package/package.json +6 -5
  3. package/scaffold/.agent/skills/total-recall/SKILL.md +1 -0
  4. package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/SKILL.md +3 -2
  5. package/scaffold/.agent/skills/total-recall/skills/{research → tr-research}/SKILL.md +2 -1
  6. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/SKILL.md +3 -2
  7. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/evals/evals.json +3 -3
  8. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/hooks/on-skill-add.sh +1 -1
  9. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/hooks/on-skill-delete.sh +1 -1
  10. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/create-skill.sh +3 -3
  11. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/find-skills.mjs +25 -1
  12. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/install-skill.mjs +117 -0
  13. package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/scan-skill.mjs +20 -0
  14. package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/SKILL.md +120 -19
  15. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +124 -0
  16. package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/references/ssss-spec.md +474 -43
  17. package/src/cli/connect.mjs +6 -59
  18. package/src/cli/ingest-openwiki.mjs +122 -0
  19. package/src/cli/ingest.mjs +9 -0
  20. package/src/cli/init.mjs +42 -2
  21. package/src/cli/skill-projection.mjs +214 -0
  22. package/src/cli/skill.mjs +44 -18
  23. package/src/core/daemon-loop.mjs +13 -2
  24. package/src/core/operation-validator.mjs +85 -9
  25. package/src/core/research.mjs +47 -9
  26. package/src/core/runtime.mjs +14 -7
  27. package/src/core/schema.mjs +85 -13
  28. package/src/server/api.mjs +2 -2
  29. package/src/server/rest.mjs +3 -3
  30. package/src/server/routes/skills.mjs +25 -4
  31. package/scaffold/.agent/skills/total-recall/skills/ssss/references/CHANGELOG.md +0 -9
  32. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/agents.yml +0 -0
  33. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/evals/.gitkeep +0 -0
  34. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/antigravity.txt +0 -0
  35. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/claude.txt +0 -0
  36. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/codex.txt +0 -0
  37. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/references/gemini.txt +0 -0
  38. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/scripts/.gitkeep +0 -0
  39. /package/scaffold/.agent/skills/total-recall/skills/{cli-agents → tr-cli-agents}/subagents/.gitkeep +0 -0
  40. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/assets/skill-template/SKILL.md +0 -0
  41. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/learnings/.gitkeep +0 -0
  42. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/references/.gitkeep +0 -0
  43. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/.gitkeep +0 -0
  44. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/enforce-skill-optimization.mjs +0 -0
  45. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/scripts/skill-watcher.mjs +0 -0
  46. /package/scaffold/.agent/skills/total-recall/skills/{skill → tr-skill}/subagents/audit-all-skills.md +0 -0
  47. /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/evals/evals.json +0 -0
  48. /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/scripts/validate-schema.mjs +0 -0
  49. /package/scaffold/.agent/skills/total-recall/skills/{ssss → tr-ssss}/subagents/audit-ssss-node.md +0 -0
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ // Antigravity CLI Agent Wrapper — routes through the running Antigravity IDE
3
+ // via agentapi, using the user's logged-in Google account.
2
4
 
3
5
  import fs from 'node:fs';
4
6
  import path from 'node:path';
5
7
  import os from 'node:os';
8
+ import { spawnSync } from 'node:child_process';
6
9
 
7
10
  const args = process.argv.slice(2);
8
11
  let prompt = '';
9
12
  let outputJson = false;
10
- let requestedModel = process.env.TR_CLI_MODEL || 'default';
13
+ let requestedModel = process.env.TR_CLI_MODEL || 'flash';
11
14
 
12
15
  if (args.includes('-h') || args.includes('--help')) {
13
16
  console.log(`
14
- Antigravity CLI Agent Wrapper — Headless Zero-Dependency Cognitive Agent
17
+ Antigravity CLI Agent Wrapper — Headless via agentapi (logged-in account)
15
18
 
16
19
  Usage:
17
20
  antigravity [options] [prompt]
@@ -19,39 +22,30 @@ Usage:
19
22
 
20
23
  Options:
21
24
  -p, --prompt The text prompt to generate content for
22
- -m, --model Gemini API model identifier or alias (default: default / gemini-3.5-flash)
23
- Supported aliases:
24
- - default / gemini / flash / gemini-flash -> dynamically resolves best Flash model
25
- - pro / gemini-pro / 3.1-pro -> dynamically resolves best Pro model
26
- -o, --json Output dynamic JSON response payload (choices: stream/json/default)
27
- --json Output dynamic JSON response payload
25
+ -m, --model Model: flash_lite, flash, pro (default: flash)
26
+ -o json Output JSON response payload
27
+ --json Output JSON response payload
28
28
  -h, --help Show this help information
29
-
30
- Examples:
31
- antigravity "Say hello"
32
- antigravity -p "Write an essay about AI" -m pro
33
- antigravity -p "Get system stats" --json
34
29
  `);
35
30
  process.exit(0);
36
31
  }
37
32
 
33
+ // Ignored flags for compatibility with tr-cli-agents dispatch
34
+ const IGNORED = new Set(['--sandbox=false', '--yolo']);
35
+
38
36
  for (let i = 0; i < args.length; i++) {
39
- if (args[i] === '-p' || args[i] === '--prompt') {
40
- prompt = args[i + 1] || '';
41
- i++;
42
- } else if (args[i] === '-o' && args[i + 1] === 'json') {
43
- outputJson = true;
44
- i++;
45
- } else if (args[i] === '--json') {
46
- outputJson = true;
47
- } else if (args[i] === '-m' || args[i] === '--model') {
48
- requestedModel = args[i + 1] || requestedModel;
49
- i++;
50
- }
37
+ if (IGNORED.has(args[i])) continue;
38
+ if (args[i] === '-p' || args[i] === '--prompt') { prompt = args[i + 1] || ''; i++; }
39
+ else if (args[i] === '-o' && args[i + 1] === 'json') { outputJson = true; i++; }
40
+ else if (args[i] === '--json') { outputJson = true; }
41
+ else if (args[i] === '-m' || args[i] === '--model') { requestedModel = args[i + 1] || requestedModel; i++; }
51
42
  }
52
43
 
53
44
  if (!prompt && args.length > 0) {
54
- prompt = args[args.length - 1];
45
+ // Last non-flag arg is the prompt
46
+ for (let i = args.length - 1; i >= 0; i--) {
47
+ if (!args[i].startsWith('-') && !IGNORED.has(args[i])) { prompt = args[i]; break; }
48
+ }
55
49
  }
56
50
 
57
51
  if (!prompt) {
@@ -59,165 +53,50 @@ if (!prompt) {
59
53
  process.exit(1);
60
54
  }
61
55
 
62
- // Robust fallback to load GOOGLE_API_KEY from .env files when executed standalone
63
- if (!process.env.GOOGLE_API_KEY) {
64
- try {
65
- let dir = process.cwd();
66
- while (dir !== path.dirname(dir)) {
67
- const envPath = path.join(dir, '.env');
68
- if (fs.existsSync(envPath)) {
69
- const content = fs.readFileSync(envPath, 'utf8');
70
- for (const line of content.split('\n')) {
71
- const match = line.match(/^\s*GOOGLE_API_KEY\s*=\s*(["']?)(.*?)\1\s*$/);
72
- if (match) {
73
- process.env.GOOGLE_API_KEY = match[2];
74
- break;
75
- }
76
- }
77
- }
78
- if (process.env.GOOGLE_API_KEY) break;
79
- dir = path.dirname(dir);
80
- }
81
- } catch {
82
- // Silent ignore
83
- }
56
+ // Map model aliases
57
+ const modelMap = {
58
+ default: 'flash', gemini: 'flash', 'gemini-flash': 'flash',
59
+ 'gemini-3.5-flash': 'flash', '3.5-flash': 'flash',
60
+ pro: 'pro', 'gemini-pro': 'pro', '3.1-pro': 'pro',
61
+ lite: 'flash_lite', 'flash-lite': 'flash_lite',
62
+ };
63
+ const model = modelMap[requestedModel] || requestedModel;
64
+
65
+ // Spawn agentapi new-conversation and capture the response
66
+ const result = spawnSync('agentapi', [
67
+ 'new-conversation',
68
+ `--model=${model}`,
69
+ '--title=antigravity-cli',
70
+ prompt,
71
+ ], {
72
+ encoding: 'utf8',
73
+ timeout: 300_000,
74
+ maxBuffer: 10 * 1024 * 1024,
75
+ env: process.env,
76
+ });
77
+
78
+ if (result.status !== 0) {
79
+ const err = result.stderr || result.stdout || 'agentapi failed';
80
+ console.error('Error:', err);
81
+ process.exit(1);
84
82
  }
85
83
 
86
- const apiKey = process.env.GOOGLE_API_KEY || '';
87
- if (!apiKey) {
88
- console.warn('Warning: GOOGLE_API_KEY environment variable is not configured. Attempting connection via GCP Application Default Credentials or ambient environment...');
84
+ const raw = result.stdout?.trim() || '';
85
+
86
+ // Parse agentapi response
87
+ let text = raw;
88
+ try {
89
+ const parsed = JSON.parse(raw);
90
+ // agentapi returns { response: { conversationId, content } }
91
+ if (parsed.response?.content) text = parsed.response.content;
92
+ else if (parsed.content) text = parsed.content;
93
+ else if (parsed.response) text = typeof parsed.response === 'string' ? parsed.response : JSON.stringify(parsed.response);
94
+ } catch {
95
+ // Not JSON, use raw output
89
96
  }
90
97
 
91
- // Resolve the model dynamically from the API registry based on the category/alias selected
92
- async function resolveGenerativeModel(selectedModel) {
93
- const aliases = {
94
- gemini: 'flash',
95
- default: 'flash',
96
- flash: 'flash',
97
- 'gemini-flash': 'flash',
98
- '3.5-flash': 'flash',
99
- pro: 'pro',
100
- 'gemini-pro': 'pro',
101
- '3.1-pro': 'pro'
102
- };
103
-
104
- const category = aliases[selectedModel];
105
- if (!category) {
106
- // If it's a full model string, use it directly
107
- return selectedModel;
108
- }
109
-
110
- try {
111
- const keyParam = apiKey ? `key=${apiKey}` : '';
112
- const headers = {};
113
- if (!apiKey && process.env.GCLOUD_TOKEN) {
114
- headers['Authorization'] = `Bearer ${process.env.GCLOUD_TOKEN}`;
115
- }
116
- const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?${keyParam}`, {
117
- headers,
118
- signal: AbortSignal.timeout(5000)
119
- });
120
- if (res.ok) {
121
- const data = await res.json();
122
- const models = data.models || [];
123
-
124
- if (category === 'pro') {
125
- const proPreferences = [
126
- 'gemini-3.1-pro-preview',
127
- 'gemini-2.5-pro',
128
- 'gemini-1.5-pro',
129
- 'gemini-pro-latest'
130
- ];
131
- for (const p of proPreferences) {
132
- if (models.some(m => m.name === `models/${p}`)) {
133
- return p;
134
- }
135
- }
136
- } else {
137
- const flashPreferences = [
138
- 'gemini-3.5-flash',
139
- 'gemini-3.1-flash-lite',
140
- 'gemini-2.5-flash',
141
- 'gemini-2.0-flash-lite',
142
- 'gemini-flash-latest'
143
- ];
144
- for (const p of flashPreferences) {
145
- if (models.some(m => m.name === `models/${p}`)) {
146
- return p;
147
- }
148
- }
149
- }
150
- }
151
- } catch {
152
- // Silent failover to hardcoded default targets
153
- }
154
-
155
- // Ultimate fallback
156
- return category === 'pro' ? 'gemini-3.1-pro-preview' : 'gemini-3.5-flash';
98
+ if (outputJson) {
99
+ console.log(JSON.stringify({ response: text }));
100
+ } else {
101
+ console.log(text);
157
102
  }
158
-
159
- async function main() {
160
- try {
161
- const resolvedModel = await resolveGenerativeModel(requestedModel);
162
- const keyParam = apiKey ? `?key=${apiKey}` : '';
163
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${resolvedModel}:generateContent${keyParam}`;
164
-
165
- const headers = {
166
- 'Content-Type': 'application/json',
167
- };
168
- if (!apiKey && process.env.GCLOUD_TOKEN) {
169
- headers['Authorization'] = `Bearer ${process.env.GCLOUD_TOKEN}`;
170
- }
171
-
172
- const res = await fetch(url, {
173
- method: 'POST',
174
- headers,
175
- body: JSON.stringify({
176
- contents: [{
177
- parts: [{ text: prompt }]
178
- }]
179
- })
180
- });
181
-
182
- if (!res.ok) {
183
- const errText = await res.text();
184
- throw new Error(`Gemini API returned status ${res.status}: ${errText}`);
185
- }
186
-
187
- const data = await res.json();
188
- const text = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
189
-
190
- // Save usage data for token monitoring/budget safety
191
- try {
192
- const promptTokens = data.usageMetadata?.promptTokenCount || 0;
193
- const candidatesTokens = data.usageMetadata?.candidatesTokenCount || 0;
194
- if (promptTokens > 0 || candidatesTokens > 0) {
195
- const tmpDir = path.join(os.homedir(), '.gemini', 'tmp', 'antigravity', 'chats');
196
- if (!fs.existsSync(tmpDir)) {
197
- fs.mkdirSync(tmpDir, { recursive: true });
198
- }
199
- const usageLine = JSON.stringify({
200
- timestamp: new Date().toISOString(),
201
- tokens: {
202
- input: promptTokens,
203
- output: candidatesTokens
204
- }
205
- });
206
- fs.appendFileSync(path.join(tmpDir, 'usage.jsonl'), usageLine + '\n', 'utf8');
207
- }
208
- } catch {
209
- // Silent ignore
210
- }
211
-
212
- if (outputJson) {
213
- console.log(JSON.stringify({ response: text }));
214
- } else {
215
- console.log(text);
216
- }
217
- } catch (err) {
218
- console.error('Error calling Gemini API:', err.message);
219
- process.exit(1);
220
- }
221
- }
222
-
223
- main();
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.9.0",
3
+ "version": "3.11.0",
4
4
  "description": "Sovereign AI system — database-free, SSSS-powered personal intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "total-recall": "./bin/total-recall.mjs",
8
- "antigravity": "./bin/antigravity.mjs"
7
+ "total-recall": "bin/total-recall.mjs",
8
+ "antigravity": "bin/antigravity.mjs"
9
9
  },
10
10
  "files": [
11
11
  "bin/",
@@ -56,6 +56,7 @@
56
56
  },
57
57
  "homepage": "https://github.com/gregiteen/total-recall#readme",
58
58
  "dependencies": {
59
+ "@ssss/cli": "github:gregiteen/ssss#v0.7.0",
59
60
  "bcrypt": "^6.0.0",
60
61
  "cookie-parser": "^1.4.7",
61
62
  "cors": "^2.8.6",
@@ -64,9 +65,9 @@
64
65
  "gray-matter": "^4.0.3",
65
66
  "jsonwebtoken": "^9.0.3",
66
67
  "natural": "^8.1.1",
68
+ "ws": "^8.18.0",
67
69
  "yaml": "^2.9.0",
68
- "zod": "^4.4.3",
69
- "ws": "^8.18.0"
70
+ "zod": "^4.4.3"
70
71
  },
71
72
  "devDependencies": {
72
73
  "skills": "^1.5.7",
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: total-recall
3
+ provenance: total-recall
3
4
  description: "Use this skill as the master guide to understand the entire Total Recall Sovereign AI OS setup, VFS topologies, SSSS protocol, CLI parameter reference, troubleshooting, and automated upstream repository sync. MANDATORY: Read this file before attempting major setup modifications or diagnoses."
4
5
  ---
5
6
 
@@ -1,5 +1,6 @@
1
1
  ---
2
- name: cli-agents
2
+ name: tr-cli-agents
3
+ provenance: total-recall
3
4
  description: "Orchestrate headlessly spawned CLI agents from the central registry."
4
5
  ---
5
6
 
@@ -12,7 +13,7 @@ This system skill governs the discovery, configuration, and execution of headles
12
13
  ## 🎯 SYSTEM OVERVIEW
13
14
 
14
15
  The CLI Agent Dispatch Engine parses agent profiles from the central registry file:
15
- `.agent/skills/total-recall/skills/cli-agents/agents.yml`
16
+ `.agent/skills/total-recall/skills/tr-cli-agents/agents.yml`
16
17
 
17
18
  ```mermaid
18
19
  flowchart TD
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  type: skill
3
- name: research
3
+ name: tr-research
4
+ provenance: total-recall
4
5
  description: "Use this skill when queueing, searching, and managing long-horizon background research projects via the Total Recall REST API."
5
6
  ---
6
7
 
@@ -1,5 +1,6 @@
1
1
  ---
2
- name: skill
2
+ name: tr-skill
3
+ provenance: total-recall
3
4
  description: "Use this skill when creating, auditing, or modifying any skill in the .agent/skills/ ecosystem. MANDATORY: You MUST read the full SKILL.md file before executing."
4
5
  ---
5
6
 
@@ -186,7 +187,7 @@ Contains standalone prompt files for subtasks that can be delegated to parallel
186
187
 
187
188
  ### Quick method (script):
188
189
  ```bash
189
- bash .agent/skills/skill/scripts/create-skill.sh my-new-skill
190
+ bash .agent/skills/tr-skill/scripts/create-skill.sh my-new-skill
190
191
  ```
191
192
 
192
193
  ### Manual method:
@@ -2,19 +2,19 @@
2
2
  {
3
3
  "id": "skill-eval-1",
4
4
  "description": "SKILL.md exists",
5
- "command": "test -f .agent/skills/skill/SKILL.md",
5
+ "command": "test -f .agent/skills/tr-skill/SKILL.md",
6
6
  "expected_exit_code": 0
7
7
  },
8
8
  {
9
9
  "id": "skill-eval-2",
10
10
  "description": "Evals validation",
11
- "command": "test -f .agent/skills/skill/scripts/run-evals.mjs",
11
+ "command": "test -f .agent/skills/tr-skill/scripts/run-evals.mjs",
12
12
  "expected_exit_code": 0
13
13
  },
14
14
  {
15
15
  "id": "skill-eval-3",
16
16
  "description": "Optimization enforcement",
17
- "command": "test -f .agent/skills/skill/scripts/enforce-skill-optimization.mjs",
17
+ "command": "test -f .agent/skills/tr-skill/scripts/enforce-skill-optimization.mjs",
18
18
  "expected_exit_code": 0
19
19
  }
20
20
  ]
@@ -10,5 +10,5 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
10
  ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
11
11
 
12
12
  echo "🔄 OnSkillAdd hook triggered — regenerating skill index..."
13
- node "$ROOT/.agent/skills/skill/scripts/update-skill-index.mjs"
13
+ node "$ROOT/.agent/skills/tr-skill/scripts/update-skill-index.mjs"
14
14
  echo "✅ Routing table and INSTRUCTIONS.md updated."
@@ -10,5 +10,5 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
10
  ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
11
11
 
12
12
  echo "🧹 OnSkillDelete hook triggered — cleaning skill index..."
13
- node "$ROOT/.agent/skills/skill/scripts/update-skill-index.mjs"
13
+ node "$ROOT/.agent/skills/tr-skill/scripts/update-skill-index.mjs"
14
14
  echo "✅ Routing table and INSTRUCTIONS.md cleaned."
@@ -1,8 +1,8 @@
1
1
  #!/bin/bash
2
2
  # create-skill.sh — Scaffold a new Dev Skill from the template
3
3
  #
4
- # Usage: bash .agent/skills/skill/scripts/create-skill.sh <skill-name>
5
- # Example: bash .agent/skills/skill/scripts/create-skill.sh my-new-skill
4
+ # Usage: bash .agent/skills/tr-skill/scripts/create-skill.sh <skill-name>
5
+ # Example: bash .agent/skills/tr-skill/scripts/create-skill.sh my-new-skill
6
6
 
7
7
  set -euo pipefail
8
8
 
@@ -60,6 +60,6 @@ echo ""
60
60
  echo "📝 Next steps:"
61
61
  echo " 1. Edit $TARGET/SKILL.md with real instructions"
62
62
  echo " 2. Write a trigger-optimized description in the frontmatter"
63
- echo " 3. Run: node .agent/skills/skill/scripts/update-skill-index.mjs"
63
+ echo " 3. Run: node .agent/skills/tr-skill/scripts/update-skill-index.mjs"
64
64
  echo ""
65
65
  echo "🏁 Done."
@@ -77,6 +77,7 @@ export function parseFindOutput(rawOutput) {
77
77
 
78
78
  const name = skillMatch[1];
79
79
  const installs = parseInstalls(skillMatch[2], skillMatch[3] || '');
80
+ const installsStr = `${skillMatch[2]}${skillMatch[3] || ''}`;
80
81
 
81
82
  // Next non-empty line should be the URL (prefixed with └ or similar box-drawing chars)
82
83
  let url = '';
@@ -92,12 +93,19 @@ export function parseFindOutput(rawOutput) {
92
93
  break; // only check the very next non-empty line
93
94
  }
94
95
 
95
- results.push({ name, installs, url });
96
+ results.push({ name, installs, installsStr, url });
96
97
  }
97
98
 
98
99
  return results;
99
100
  }
100
101
 
102
+ function formatInstalls(installs) {
103
+ if (installs >= 1_000_000_000) return `${(installs / 1_000_000_000).toFixed(1).replace(/\.0$/, '')}B`;
104
+ if (installs >= 1_000_000) return `${(installs / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`;
105
+ if (installs >= 1_000) return `${(installs / 1_000).toFixed(1).replace(/\.0$/, '')}K`;
106
+ return String(installs);
107
+ }
108
+
101
109
  // ─── Live Registry Search ───────────────────────────────────────────────────
102
110
 
103
111
  /**
@@ -122,3 +130,19 @@ export function searchRegistry(query, { timeoutMs = 15_000 } = {}) {
122
130
  return [];
123
131
  }
124
132
  }
133
+
134
+ /**
135
+ * Search skills.sh and sort by absolute install count, highest first.
136
+ *
137
+ * @param {string} query
138
+ * @param {object} [options]
139
+ * @returns {Array<{ name: string, installs: number, installsStr: string, url: string }>}
140
+ */
141
+ export function searchAndSort(query, options = {}) {
142
+ return searchRegistry(query, options)
143
+ .map(result => ({
144
+ ...result,
145
+ installsStr: result.installsStr || formatInstalls(result.installs || 0),
146
+ }))
147
+ .sort((a, b) => (b.installs || 0) - (a.installs || 0));
148
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * install-skill.mjs — safe wrapper around the Skills CLI installer.
3
+ */
4
+
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { scanDirectory, formatReport } from './scan-skill.mjs';
9
+
10
+ const BLOCKED_SOURCE_CHARS = /[\0\r\n;&|<>`$]/;
11
+
12
+ export function validateSkillSource(source) {
13
+ if (typeof source !== 'string') return false;
14
+ const trimmed = source.trim();
15
+ if (!trimmed || trimmed.length > 500) return false;
16
+ return !BLOCKED_SOURCE_CHARS.test(trimmed);
17
+ }
18
+
19
+ export function inferSkillName(source) {
20
+ const trimmed = String(source || '').trim().replace(/\/+$/, '');
21
+ if (!trimmed) return null;
22
+ if (!trimmed.startsWith('git@') && trimmed.includes('@')) {
23
+ return trimmed.split('@').pop() || null;
24
+ }
25
+ const last = trimmed.split('/').filter(Boolean).pop();
26
+ return last ? last.replace(/\.git$/i, '') : null;
27
+ }
28
+
29
+ function uniqueExistingDirs(paths) {
30
+ const seen = new Set();
31
+ const dirs = [];
32
+ for (const candidate of paths) {
33
+ if (!candidate) continue;
34
+ const resolved = path.resolve(candidate);
35
+ if (seen.has(resolved) || !fs.existsSync(resolved)) continue;
36
+ seen.add(resolved);
37
+ dirs.push(resolved);
38
+ }
39
+ return dirs;
40
+ }
41
+
42
+ export function installedSkillCandidates(source, { cwd = process.cwd(), agentDir = path.join(cwd, '.agent') } = {}) {
43
+ const skillName = inferSkillName(source);
44
+ if (!skillName) return [];
45
+ return [
46
+ path.join(agentDir, 'skills', skillName),
47
+ path.join(cwd, '.agent', 'skills', skillName),
48
+ path.join(cwd, '.agents', 'skills', skillName),
49
+ path.join(cwd, '.claude', 'skills', skillName),
50
+ ];
51
+ }
52
+
53
+ /**
54
+ * Install a skill with the Skills CLI, then scan installed script files.
55
+ *
56
+ * @param {string} source skills add source, e.g. owner/repo@skill or URL
57
+ * @param {object} [options]
58
+ * @param {string} [options.cwd]
59
+ * @param {string} [options.agentDir]
60
+ * @param {boolean} [options.dryRun=false]
61
+ * @returns {{ success: boolean, source: string, command?: string[], findings?: Array, error?: string }}
62
+ */
63
+ export function installSkill(source, options = {}) {
64
+ const cwd = options.cwd || process.cwd();
65
+ const agentDir = options.agentDir || path.join(cwd, '.agent');
66
+ const normalized = String(source || '').trim();
67
+
68
+ if (!validateSkillSource(normalized)) {
69
+ const error = 'Invalid skill source. Use a repository, URL, git source, local path, or owner/repo@skill without shell metacharacters.';
70
+ console.error(`❌ ${error}`);
71
+ return { success: false, source: normalized, error };
72
+ }
73
+
74
+ const command = ['npx', '-y', 'skills', 'add', normalized, '-y'];
75
+ if (options.dryRun) {
76
+ return { success: true, source: normalized, command };
77
+ }
78
+
79
+ console.log(`📦 Installing skill: ${normalized}`);
80
+ const child = spawnSync(command[0], command.slice(1), {
81
+ cwd,
82
+ encoding: 'utf8',
83
+ stdio: ['ignore', 'pipe', 'pipe'],
84
+ });
85
+
86
+ if (child.stdout) process.stdout.write(child.stdout);
87
+ if (child.stderr) process.stderr.write(child.stderr);
88
+
89
+ if (child.error || child.status !== 0) {
90
+ const error = child.error?.message || `skills CLI exited with status ${child.status}`;
91
+ console.error(`❌ Skill installation failed: ${error}`);
92
+ return { success: false, source: normalized, command, error };
93
+ }
94
+
95
+ const installedDirs = uniqueExistingDirs(installedSkillCandidates(normalized, { cwd, agentDir }));
96
+ const findings = installedDirs.flatMap(dir => scanDirectory(dir));
97
+ const report = formatReport(findings);
98
+ console.log(report.trimEnd());
99
+
100
+ if (findings.some(f => f.severity === 'CRITICAL')) {
101
+ const error = 'Installed skill contains critical scanner findings.';
102
+ console.error(`❌ ${error}`);
103
+ return { success: false, source: normalized, command, findings, error };
104
+ }
105
+
106
+ const compile = spawnSync('npx', ['total-recall', 'compile'], {
107
+ cwd,
108
+ encoding: 'utf8',
109
+ stdio: ['ignore', 'pipe', 'pipe'],
110
+ env: { ...process.env, AGENT_DIR: agentDir },
111
+ });
112
+ if (compile.status !== 0) {
113
+ console.warn('⚠️ Skill installed, but surface recompilation returned a warning.');
114
+ }
115
+
116
+ return { success: true, source: normalized, command, findings };
117
+ }
@@ -205,3 +205,23 @@ export function formatReport(findings) {
205
205
 
206
206
  return report;
207
207
  }
208
+
209
+ /**
210
+ * CLI-friendly scanner entrypoint.
211
+ *
212
+ * @param {string} targetDir
213
+ * @returns {{ success: boolean, findings: Array, report: string }}
214
+ */
215
+ export function runScan(targetDir) {
216
+ if (!targetDir || !fs.existsSync(targetDir)) {
217
+ const report = `Skill directory not found: ${targetDir}\n`;
218
+ console.error(report.trim());
219
+ return { success: false, findings: [], report };
220
+ }
221
+
222
+ const findings = scanDirectory(targetDir);
223
+ const report = formatReport(findings);
224
+ const blocked = findings.some(f => f.severity === 'CRITICAL');
225
+ console.log(report.trimEnd());
226
+ return { success: !blocked, findings, report };
227
+ }