@intent-systems/nexus 2026.1.5-3 → 2026.1.5-5

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 (144) hide show
  1. package/dist/agents/agent-id.js +41 -0
  2. package/dist/agents/auth-profiles.js +114 -25
  3. package/dist/agents/identity-state.js +79 -0
  4. package/dist/agents/model-auth.js +1 -0
  5. package/dist/agents/model-fallback.js +15 -9
  6. package/dist/agents/model-selection.js +1 -1
  7. package/dist/agents/models-config.js +17 -11
  8. package/dist/agents/pi-embedded-runner.js +101 -9
  9. package/dist/agents/sandbox.js +12 -3
  10. package/dist/agents/skill-runner.js +29 -4
  11. package/dist/agents/skill-usage.js +114 -11
  12. package/dist/agents/skills-status.js +4 -4
  13. package/dist/agents/skills.js +18 -7
  14. package/dist/agents/subagent-registry.js +25 -11
  15. package/dist/agents/system-prompt.js +16 -0
  16. package/dist/agents/tool-policy.js +19 -3
  17. package/dist/agents/tools/browser-tool.js +5 -2
  18. package/dist/agents/tools/image-tool.js +93 -8
  19. package/dist/agents/tools/sessions-announce-target.js +5 -1
  20. package/dist/agents/workspace.js +55 -46
  21. package/dist/auto-reply/command-detection.js +2 -1
  22. package/dist/auto-reply/reply/directive-handling.js +153 -28
  23. package/dist/auto-reply/reply/directives.js +17 -2
  24. package/dist/auto-reply/reply/model-selection.js +8 -3
  25. package/dist/auto-reply/reply/queue.js +2 -2
  26. package/dist/auto-reply/reply.js +1 -1
  27. package/dist/auto-reply/thinking.js +15 -0
  28. package/dist/browser/chrome.js +1 -1
  29. package/dist/browser/client.js +2 -0
  30. package/dist/browser/config.js +6 -2
  31. package/dist/browser/pw-tools-core.js +3 -0
  32. package/dist/browser/routes/agent.js +14 -0
  33. package/dist/canvas-host/server.js +1 -1
  34. package/dist/capabilities/detector.js +245 -0
  35. package/dist/capabilities/registry.js +99 -0
  36. package/dist/channels/location.js +44 -0
  37. package/dist/channels/web/index.js +2 -0
  38. package/dist/cli/cloud-cli.js +12 -7
  39. package/dist/cli/credential-cli.js +139 -17
  40. package/dist/cli/gateway-cli.js +1 -1
  41. package/dist/cli/log-cli.js +25 -0
  42. package/dist/cli/pairing-cli.js +1 -1
  43. package/dist/cli/program.js +58 -6
  44. package/dist/cli/run-main.js +1 -1
  45. package/dist/cli/skills-cli.js +144 -21
  46. package/dist/cli/skills-hub-cli.js +59 -29
  47. package/dist/cli/tool-connector-cli.js +99 -24
  48. package/dist/cli/upstream-sync-cli.js +253 -96
  49. package/dist/cli/usage-cli.js +14 -0
  50. package/dist/commands/auth-choice-options.js +6 -1
  51. package/dist/commands/auth-choice.js +157 -5
  52. package/dist/commands/bootstrap-preset.js +10 -6
  53. package/dist/commands/capabilities.js +33 -6
  54. package/dist/commands/claude-md.js +3 -2
  55. package/dist/commands/config-view.js +1 -1
  56. package/dist/commands/configure.js +4 -4
  57. package/dist/commands/credential.js +497 -36
  58. package/dist/commands/cursor-rules.js +39 -19
  59. package/dist/commands/doctor.js +5 -4
  60. package/dist/commands/identity.js +28 -31
  61. package/dist/commands/init.js +15 -18
  62. package/dist/commands/log.js +134 -0
  63. package/dist/commands/models/fallbacks.js +1 -1
  64. package/dist/commands/models/image-fallbacks.js +1 -1
  65. package/dist/commands/models/list.js +1 -1
  66. package/dist/commands/models/scan.js +1 -1
  67. package/dist/commands/onboard-auth.js +27 -2
  68. package/dist/commands/onboard-eve-identity.js +7 -8
  69. package/dist/commands/onboard-non-interactive.js +4 -2
  70. package/dist/commands/onboard-quickstart.js +18 -11
  71. package/dist/commands/quest-state.js +271 -0
  72. package/dist/commands/quest.js +53 -13
  73. package/dist/commands/reset.js +1 -1
  74. package/dist/commands/sessions-ingest.js +5 -4
  75. package/dist/commands/setup.js +4 -2
  76. package/dist/commands/skills-manifest.js +2 -2
  77. package/dist/commands/status.js +179 -61
  78. package/dist/commands/suggestions.js +1 -1
  79. package/dist/commands/usage-tracking.js +32 -0
  80. package/dist/commands/usage-upload.js +6 -1
  81. package/dist/config/defaults.js +1 -3
  82. package/dist/config/includes.js +5 -7
  83. package/dist/config/io.js +88 -16
  84. package/dist/config/legacy.js +4 -2
  85. package/dist/config/paths.js +16 -0
  86. package/dist/config/sessions.js +9 -5
  87. package/dist/config/zod-schema.js +4 -3
  88. package/dist/control-plane/broker/broker.js +1022 -0
  89. package/dist/control-plane/compaction.js +282 -0
  90. package/dist/control-plane/factory.js +31 -0
  91. package/dist/control-plane/index.js +10 -0
  92. package/dist/control-plane/odu/agents.js +192 -0
  93. package/dist/control-plane/odu/interaction-tools.js +208 -0
  94. package/dist/control-plane/odu/prompt-loader.js +95 -0
  95. package/dist/control-plane/odu/runtime.js +479 -0
  96. package/dist/control-plane/odu/types.js +6 -0
  97. package/dist/control-plane/odu-control-plane.js +316 -0
  98. package/dist/control-plane/single-agent.js +249 -0
  99. package/dist/control-plane/types.js +11 -0
  100. package/dist/credentials/store.js +449 -0
  101. package/dist/gateway/server-browser.js +5 -4
  102. package/dist/gateway/server-methods/cron.js +11 -1
  103. package/dist/gateway/server.js +14 -7
  104. package/dist/infra/bonjour.js +1 -1
  105. package/dist/infra/event-log.js +8 -2
  106. package/dist/infra/path-env.js +1 -2
  107. package/dist/infra/provider-usage.auth.js +5 -3
  108. package/dist/infra/provider-usage.fetch.claude.js +16 -6
  109. package/dist/infra/provider-usage.fetch.minimax.js +8 -3
  110. package/dist/infra/provider-usage.js +9 -5
  111. package/dist/infra/restart.js +2 -2
  112. package/dist/infra/usage-settings.js +78 -0
  113. package/dist/infra/usage-suggestions.js +17 -5
  114. package/dist/infra/usage-upload.js +38 -1
  115. package/dist/infra/voicewake.js +2 -2
  116. package/dist/logging/redact.js +109 -0
  117. package/dist/markdown/fences.js +58 -0
  118. package/dist/media/image-ops.js +3 -1
  119. package/dist/memory/embeddings.js +146 -0
  120. package/dist/memory/index.js +3 -0
  121. package/dist/memory/internal.js +163 -0
  122. package/dist/pairing/pairing-store.js +218 -0
  123. package/dist/plugins/cli.js +42 -0
  124. package/dist/plugins/discovery.js +253 -0
  125. package/dist/plugins/install.js +181 -0
  126. package/dist/plugins/loader.js +290 -0
  127. package/dist/plugins/registry.js +105 -0
  128. package/dist/plugins/status.js +29 -0
  129. package/dist/plugins/tools.js +39 -0
  130. package/dist/plugins/types.js +1 -0
  131. package/dist/providers/github-copilot-auth.js +1 -1
  132. package/dist/routing/resolve-route.js +144 -0
  133. package/dist/routing/session-key.js +65 -0
  134. package/dist/sessions/send-policy.js +5 -5
  135. package/dist/slack/monitor.js +22 -1
  136. package/dist/telegram/reaction-level.js +2 -1
  137. package/dist/utils/provider-utils.js +28 -0
  138. package/dist/utils.js +4 -3
  139. package/dist/wizard/onboarding.js +29 -7
  140. package/package.json +4 -29
  141. package/patches/@mariozechner__pi-ai.patch +215 -0
  142. package/patches/playwright-core@1.57.0.patch +13 -0
  143. package/patches/qrcode-terminal.patch +12 -0
  144. package/scripts/postinstall.js +202 -0
@@ -0,0 +1,163 @@
1
+ import crypto from "node:crypto";
2
+ import fsSync from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ export function ensureDir(dir) {
6
+ try {
7
+ fsSync.mkdirSync(dir, { recursive: true });
8
+ }
9
+ catch { }
10
+ return dir;
11
+ }
12
+ export function normalizeRelPath(value) {
13
+ const trimmed = value.trim().replace(/^[./]+/, "");
14
+ return trimmed.replace(/\\/g, "/");
15
+ }
16
+ export function isMemoryPath(relPath) {
17
+ const normalized = normalizeRelPath(relPath);
18
+ if (!normalized)
19
+ return false;
20
+ if (normalized === "MEMORY.md" || normalized === "memory.md")
21
+ return true;
22
+ return normalized.startsWith("memory/");
23
+ }
24
+ async function exists(filePath) {
25
+ try {
26
+ await fs.access(filePath);
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ async function walkDir(dir, files) {
34
+ const entries = await fs.readdir(dir, { withFileTypes: true });
35
+ for (const entry of entries) {
36
+ const full = path.join(dir, entry.name);
37
+ if (entry.isDirectory()) {
38
+ await walkDir(full, files);
39
+ continue;
40
+ }
41
+ if (!entry.isFile())
42
+ continue;
43
+ if (!entry.name.endsWith(".md"))
44
+ continue;
45
+ files.push(full);
46
+ }
47
+ }
48
+ export async function listMemoryFiles(workspaceDir) {
49
+ const result = [];
50
+ const memoryFile = path.join(workspaceDir, "MEMORY.md");
51
+ const altMemoryFile = path.join(workspaceDir, "memory.md");
52
+ if (await exists(memoryFile))
53
+ result.push(memoryFile);
54
+ if (await exists(altMemoryFile))
55
+ result.push(altMemoryFile);
56
+ const memoryDir = path.join(workspaceDir, "memory");
57
+ if (await exists(memoryDir)) {
58
+ await walkDir(memoryDir, result);
59
+ }
60
+ return result;
61
+ }
62
+ export function hashText(value) {
63
+ return crypto.createHash("sha256").update(value).digest("hex");
64
+ }
65
+ export async function buildFileEntry(absPath, workspaceDir) {
66
+ const stat = await fs.stat(absPath);
67
+ const content = await fs.readFile(absPath, "utf-8");
68
+ const hash = hashText(content);
69
+ return {
70
+ path: path.relative(workspaceDir, absPath).replace(/\\/g, "/"),
71
+ absPath,
72
+ mtimeMs: stat.mtimeMs,
73
+ size: stat.size,
74
+ hash,
75
+ };
76
+ }
77
+ export function chunkMarkdown(content, chunking) {
78
+ const lines = content.split("\n");
79
+ if (lines.length === 0)
80
+ return [];
81
+ const maxChars = Math.max(32, chunking.tokens * 4);
82
+ const overlapChars = Math.max(0, chunking.overlap * 4);
83
+ const chunks = [];
84
+ let current = [];
85
+ let currentChars = 0;
86
+ const flush = () => {
87
+ if (current.length === 0)
88
+ return;
89
+ const firstEntry = current[0];
90
+ const lastEntry = current[current.length - 1];
91
+ if (!firstEntry || !lastEntry)
92
+ return;
93
+ const text = current.map((entry) => entry.line).join("\n");
94
+ const startLine = firstEntry.lineNo;
95
+ const endLine = lastEntry.lineNo;
96
+ chunks.push({
97
+ startLine,
98
+ endLine,
99
+ text,
100
+ hash: hashText(text),
101
+ });
102
+ };
103
+ const carryOverlap = () => {
104
+ if (overlapChars <= 0 || current.length === 0) {
105
+ current = [];
106
+ currentChars = 0;
107
+ return;
108
+ }
109
+ let acc = 0;
110
+ const kept = [];
111
+ for (let i = current.length - 1; i >= 0; i -= 1) {
112
+ const entry = current[i];
113
+ if (!entry)
114
+ continue;
115
+ acc += entry.line.length + 1;
116
+ kept.unshift(entry);
117
+ if (acc >= overlapChars)
118
+ break;
119
+ }
120
+ current = kept;
121
+ currentChars = kept.reduce((sum, entry) => sum + entry.line.length + 1, 0);
122
+ };
123
+ for (let i = 0; i < lines.length; i += 1) {
124
+ const line = lines[i] ?? "";
125
+ const lineNo = i + 1;
126
+ const lineSize = line.length + 1;
127
+ if (currentChars + lineSize > maxChars && current.length > 0) {
128
+ flush();
129
+ carryOverlap();
130
+ }
131
+ current.push({ line, lineNo });
132
+ currentChars += lineSize;
133
+ }
134
+ flush();
135
+ return chunks;
136
+ }
137
+ export function parseEmbedding(raw) {
138
+ try {
139
+ const parsed = JSON.parse(raw);
140
+ return Array.isArray(parsed) ? parsed : [];
141
+ }
142
+ catch {
143
+ return [];
144
+ }
145
+ }
146
+ export function cosineSimilarity(a, b) {
147
+ if (a.length === 0 || b.length === 0)
148
+ return 0;
149
+ const len = Math.min(a.length, b.length);
150
+ let dot = 0;
151
+ let normA = 0;
152
+ let normB = 0;
153
+ for (let i = 0; i < len; i += 1) {
154
+ const av = a[i] ?? 0;
155
+ const bv = b[i] ?? 0;
156
+ dot += av * bv;
157
+ normA += av * av;
158
+ normB += bv * bv;
159
+ }
160
+ if (normA === 0 || normB === 0)
161
+ return 0;
162
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
163
+ }
@@ -0,0 +1,218 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
5
+ function resolveCredentialsDir(env = process.env) {
6
+ const stateDir = resolveStateDir(env, os.homedir);
7
+ return resolveOAuthDir(env, stateDir);
8
+ }
9
+ function resolvePairingPath(provider, env = process.env) {
10
+ return path.join(resolveCredentialsDir(env), `${provider}-pairing.json`);
11
+ }
12
+ function resolveAllowFromPath(provider, env = process.env) {
13
+ return path.join(resolveCredentialsDir(env), `${provider}-allowFrom.json`);
14
+ }
15
+ function safeParseJson(raw) {
16
+ try {
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ async function readJsonFile(filePath, fallback) {
24
+ try {
25
+ const raw = await fs.promises.readFile(filePath, "utf-8");
26
+ const parsed = safeParseJson(raw);
27
+ if (parsed == null)
28
+ return { value: fallback, exists: true };
29
+ return { value: parsed, exists: true };
30
+ }
31
+ catch (err) {
32
+ const code = err.code;
33
+ if (code === "ENOENT")
34
+ return { value: fallback, exists: false };
35
+ return { value: fallback, exists: false };
36
+ }
37
+ }
38
+ async function writeJsonFile(filePath, value) {
39
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
40
+ await fs.promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
41
+ }
42
+ function randomCode() {
43
+ // Human-friendly: 8 chars, upper, no ambiguous chars (0O1I).
44
+ const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
45
+ let out = "";
46
+ for (let i = 0; i < 8; i++) {
47
+ out += alphabet[Math.floor(Math.random() * alphabet.length)];
48
+ }
49
+ return out;
50
+ }
51
+ function normalizeId(value) {
52
+ return String(value).trim();
53
+ }
54
+ function normalizeAllowEntry(provider, entry) {
55
+ const trimmed = entry.trim();
56
+ if (!trimmed)
57
+ return "";
58
+ if (trimmed === "*")
59
+ return "";
60
+ if (provider === "telegram")
61
+ return trimmed.replace(/^(telegram|tg):/i, "");
62
+ if (provider === "signal")
63
+ return trimmed.replace(/^signal:/i, "");
64
+ if (provider === "discord")
65
+ return trimmed.replace(/^(discord|user):/i, "");
66
+ if (provider === "slack")
67
+ return trimmed.replace(/^(slack|user):/i, "");
68
+ return trimmed;
69
+ }
70
+ export async function readProviderAllowFromStore(provider, env = process.env) {
71
+ const filePath = resolveAllowFromPath(provider, env);
72
+ const { value } = await readJsonFile(filePath, {
73
+ version: 1,
74
+ allowFrom: [],
75
+ });
76
+ const list = Array.isArray(value.allowFrom) ? value.allowFrom : [];
77
+ return list
78
+ .map((v) => normalizeAllowEntry(provider, String(v)))
79
+ .filter(Boolean);
80
+ }
81
+ export async function addProviderAllowFromStoreEntry(params) {
82
+ const env = params.env ?? process.env;
83
+ const filePath = resolveAllowFromPath(params.provider, env);
84
+ const { value } = await readJsonFile(filePath, {
85
+ version: 1,
86
+ allowFrom: [],
87
+ });
88
+ const current = (Array.isArray(value.allowFrom) ? value.allowFrom : [])
89
+ .map((v) => normalizeAllowEntry(params.provider, String(v)))
90
+ .filter(Boolean);
91
+ const normalized = normalizeAllowEntry(params.provider, normalizeId(params.entry));
92
+ if (!normalized)
93
+ return { changed: false, allowFrom: current };
94
+ if (current.includes(normalized))
95
+ return { changed: false, allowFrom: current };
96
+ const next = [...current, normalized];
97
+ await writeJsonFile(filePath, {
98
+ version: 1,
99
+ allowFrom: next,
100
+ });
101
+ return { changed: true, allowFrom: next };
102
+ }
103
+ export async function listProviderPairingRequests(provider, env = process.env) {
104
+ const filePath = resolvePairingPath(provider, env);
105
+ const { value } = await readJsonFile(filePath, {
106
+ version: 1,
107
+ requests: [],
108
+ });
109
+ const reqs = Array.isArray(value.requests) ? value.requests : [];
110
+ return reqs
111
+ .filter((r) => r &&
112
+ typeof r.id === "string" &&
113
+ typeof r.code === "string" &&
114
+ typeof r.createdAt === "string")
115
+ .slice()
116
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
117
+ }
118
+ export async function upsertProviderPairingRequest(params) {
119
+ const env = params.env ?? process.env;
120
+ const filePath = resolvePairingPath(params.provider, env);
121
+ const { value } = await readJsonFile(filePath, {
122
+ version: 1,
123
+ requests: [],
124
+ });
125
+ const now = new Date().toISOString();
126
+ const id = normalizeId(params.id);
127
+ const meta = params.meta && typeof params.meta === "object"
128
+ ? Object.fromEntries(Object.entries(params.meta)
129
+ .map(([k, v]) => [k, String(v ?? "").trim()])
130
+ .filter(([_, v]) => Boolean(v)))
131
+ : undefined;
132
+ const reqs = Array.isArray(value.requests) ? value.requests : [];
133
+ const existingIdx = reqs.findIndex((r) => r.id === id);
134
+ if (existingIdx >= 0) {
135
+ const existing = reqs[existingIdx];
136
+ const existingCode = existing && typeof existing.code === "string" ? existing.code.trim() : "";
137
+ const code = existingCode || randomCode();
138
+ const next = {
139
+ id,
140
+ code,
141
+ createdAt: existing?.createdAt ?? now,
142
+ lastSeenAt: now,
143
+ meta: meta ?? existing?.meta,
144
+ };
145
+ reqs[existingIdx] = next;
146
+ await writeJsonFile(filePath, {
147
+ version: 1,
148
+ requests: reqs,
149
+ });
150
+ return { code, created: false };
151
+ }
152
+ const code = randomCode();
153
+ const next = {
154
+ id,
155
+ code,
156
+ createdAt: now,
157
+ lastSeenAt: now,
158
+ ...(meta ? { meta } : {}),
159
+ };
160
+ await writeJsonFile(filePath, {
161
+ version: 1,
162
+ requests: [...reqs, next],
163
+ });
164
+ return { code, created: true };
165
+ }
166
+ export async function approveProviderPairingCode(params) {
167
+ const env = params.env ?? process.env;
168
+ const code = params.code.trim().toUpperCase();
169
+ if (!code)
170
+ return null;
171
+ const filePath = resolvePairingPath(params.provider, env);
172
+ const { value } = await readJsonFile(filePath, {
173
+ version: 1,
174
+ requests: [],
175
+ });
176
+ const reqs = Array.isArray(value.requests) ? value.requests : [];
177
+ const idx = reqs.findIndex((r) => String(r.code ?? "").toUpperCase() === code);
178
+ if (idx < 0)
179
+ return null;
180
+ const entry = reqs[idx];
181
+ if (!entry)
182
+ return null;
183
+ reqs.splice(idx, 1);
184
+ await writeJsonFile(filePath, {
185
+ version: 1,
186
+ requests: reqs,
187
+ });
188
+ await addProviderAllowFromStoreEntry({
189
+ provider: params.provider,
190
+ entry: entry.id,
191
+ env,
192
+ });
193
+ return { id: entry.id, entry };
194
+ }
195
+ export const readChannelAllowFromStore = readProviderAllowFromStore;
196
+ export async function addChannelAllowFromStoreEntry(params) {
197
+ return addProviderAllowFromStoreEntry({
198
+ provider: params.channel,
199
+ entry: params.entry,
200
+ env: params.env,
201
+ });
202
+ }
203
+ export const listChannelPairingRequests = listProviderPairingRequests;
204
+ export async function upsertChannelPairingRequest(params) {
205
+ return upsertProviderPairingRequest({
206
+ provider: params.channel,
207
+ id: params.id,
208
+ meta: params.meta,
209
+ env: params.env,
210
+ });
211
+ }
212
+ export async function approveChannelPairingCode(params) {
213
+ return approveProviderPairingCode({
214
+ provider: params.channel,
215
+ code: params.code,
216
+ env: params.env,
217
+ });
218
+ }
@@ -0,0 +1,42 @@
1
+ import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
2
+ import { loadConfig } from "../config/config.js";
3
+ import { createSubsystemLogger } from "../logging.js";
4
+ import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
5
+ import { loadNexusPlugins } from "./loader.js";
6
+ const log = createSubsystemLogger("plugins");
7
+ function resolveDefaultAgentId(cfg) {
8
+ return normalizeAgentId(cfg.routing?.defaultAgentId ?? DEFAULT_AGENT_ID);
9
+ }
10
+ export function registerPluginCliCommands(program, cfg) {
11
+ const config = cfg ?? loadConfig();
12
+ const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
13
+ const logger = {
14
+ info: (msg) => log.info(msg),
15
+ warn: (msg) => log.warn(msg),
16
+ error: (msg) => log.error(msg),
17
+ debug: (msg) => log.debug(msg),
18
+ };
19
+ const registry = loadNexusPlugins({
20
+ config,
21
+ workspaceDir,
22
+ logger,
23
+ });
24
+ for (const entry of registry.cliRegistrars) {
25
+ try {
26
+ const result = entry.register({
27
+ program,
28
+ config,
29
+ workspaceDir,
30
+ logger,
31
+ });
32
+ if (result && typeof result.then === "function") {
33
+ void result.catch((err) => {
34
+ log.warn(`plugin CLI register failed (${entry.pluginId}): ${String(err)}`);
35
+ });
36
+ }
37
+ }
38
+ catch (err) {
39
+ log.warn(`plugin CLI register failed (${entry.pluginId}): ${String(err)}`);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,253 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { CONFIG_DIR, resolveUserPath } from "../utils.js";
4
+ const EXTENSION_EXTS = new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"]);
5
+ function isExtensionFile(filePath) {
6
+ const ext = path.extname(filePath);
7
+ if (!EXTENSION_EXTS.has(ext))
8
+ return false;
9
+ return !filePath.endsWith(".d.ts");
10
+ }
11
+ function readPackageManifest(dir) {
12
+ const manifestPath = path.join(dir, "package.json");
13
+ if (!fs.existsSync(manifestPath))
14
+ return null;
15
+ try {
16
+ const raw = fs.readFileSync(manifestPath, "utf-8");
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function resolvePackageExtensions(manifest) {
24
+ // Try nexus first, fall back to clawdbot for backward compat
25
+ const raw = manifest.nexus?.extensions ?? manifest.clawdbot?.extensions;
26
+ if (!Array.isArray(raw))
27
+ return [];
28
+ return raw
29
+ .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
30
+ .filter(Boolean);
31
+ }
32
+ function deriveIdHint(params) {
33
+ const base = path.basename(params.filePath, path.extname(params.filePath));
34
+ const rawPackageName = params.packageName?.trim();
35
+ if (!rawPackageName)
36
+ return base;
37
+ // Prefer the unscoped name so config keys stay stable even when the npm
38
+ // package is scoped (example: @nexus/voice-call -> voice-call).
39
+ const unscoped = rawPackageName.includes("/")
40
+ ? (rawPackageName.split("/").pop() ?? rawPackageName)
41
+ : rawPackageName;
42
+ if (!params.hasMultipleExtensions)
43
+ return unscoped;
44
+ return `${unscoped}/${base}`;
45
+ }
46
+ function addCandidate(params) {
47
+ const resolved = path.resolve(params.source);
48
+ if (params.seen.has(resolved))
49
+ return;
50
+ params.seen.add(resolved);
51
+ const manifest = params.manifest ?? null;
52
+ params.candidates.push({
53
+ idHint: params.idHint,
54
+ source: resolved,
55
+ origin: params.origin,
56
+ workspaceDir: params.workspaceDir,
57
+ packageName: manifest?.name?.trim() || undefined,
58
+ packageVersion: manifest?.version?.trim() || undefined,
59
+ packageDescription: manifest?.description?.trim() || undefined,
60
+ });
61
+ }
62
+ function discoverInDirectory(params) {
63
+ if (!fs.existsSync(params.dir))
64
+ return;
65
+ let entries = [];
66
+ try {
67
+ entries = fs.readdirSync(params.dir, { withFileTypes: true });
68
+ }
69
+ catch (err) {
70
+ params.diagnostics.push({
71
+ level: "warn",
72
+ message: `failed to read extensions dir: ${params.dir} (${String(err)})`,
73
+ source: params.dir,
74
+ });
75
+ return;
76
+ }
77
+ for (const entry of entries) {
78
+ const fullPath = path.join(params.dir, entry.name);
79
+ if (entry.isFile()) {
80
+ if (!isExtensionFile(fullPath))
81
+ continue;
82
+ addCandidate({
83
+ candidates: params.candidates,
84
+ seen: params.seen,
85
+ idHint: path.basename(entry.name, path.extname(entry.name)),
86
+ source: fullPath,
87
+ origin: params.origin,
88
+ workspaceDir: params.workspaceDir,
89
+ });
90
+ }
91
+ if (!entry.isDirectory())
92
+ continue;
93
+ const manifest = readPackageManifest(fullPath);
94
+ const extensions = manifest ? resolvePackageExtensions(manifest) : [];
95
+ if (extensions.length > 0) {
96
+ for (const extPath of extensions) {
97
+ const resolved = path.resolve(fullPath, extPath);
98
+ addCandidate({
99
+ candidates: params.candidates,
100
+ seen: params.seen,
101
+ idHint: deriveIdHint({
102
+ filePath: resolved,
103
+ packageName: manifest?.name,
104
+ hasMultipleExtensions: extensions.length > 1,
105
+ }),
106
+ source: resolved,
107
+ origin: params.origin,
108
+ workspaceDir: params.workspaceDir,
109
+ manifest,
110
+ });
111
+ }
112
+ continue;
113
+ }
114
+ const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
115
+ const indexFile = indexCandidates
116
+ .map((candidate) => path.join(fullPath, candidate))
117
+ .find((candidate) => fs.existsSync(candidate));
118
+ if (indexFile && isExtensionFile(indexFile)) {
119
+ addCandidate({
120
+ candidates: params.candidates,
121
+ seen: params.seen,
122
+ idHint: entry.name,
123
+ source: indexFile,
124
+ origin: params.origin,
125
+ workspaceDir: params.workspaceDir,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ function discoverFromPath(params) {
131
+ const resolved = resolveUserPath(params.rawPath);
132
+ if (!fs.existsSync(resolved)) {
133
+ params.diagnostics.push({
134
+ level: "warn",
135
+ message: `plugin path not found: ${resolved}`,
136
+ source: resolved,
137
+ });
138
+ return;
139
+ }
140
+ const stat = fs.statSync(resolved);
141
+ if (stat.isFile()) {
142
+ if (!isExtensionFile(resolved)) {
143
+ params.diagnostics.push({
144
+ level: "warn",
145
+ message: `plugin path is not a supported file: ${resolved}`,
146
+ source: resolved,
147
+ });
148
+ return;
149
+ }
150
+ addCandidate({
151
+ candidates: params.candidates,
152
+ seen: params.seen,
153
+ idHint: path.basename(resolved, path.extname(resolved)),
154
+ source: resolved,
155
+ origin: params.origin,
156
+ workspaceDir: params.workspaceDir,
157
+ });
158
+ return;
159
+ }
160
+ if (stat.isDirectory()) {
161
+ const manifest = readPackageManifest(resolved);
162
+ const extensions = manifest ? resolvePackageExtensions(manifest) : [];
163
+ if (extensions.length > 0) {
164
+ for (const extPath of extensions) {
165
+ const source = path.resolve(resolved, extPath);
166
+ addCandidate({
167
+ candidates: params.candidates,
168
+ seen: params.seen,
169
+ idHint: deriveIdHint({
170
+ filePath: source,
171
+ packageName: manifest?.name,
172
+ hasMultipleExtensions: extensions.length > 1,
173
+ }),
174
+ source,
175
+ origin: params.origin,
176
+ workspaceDir: params.workspaceDir,
177
+ manifest,
178
+ });
179
+ }
180
+ return;
181
+ }
182
+ const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
183
+ const indexFile = indexCandidates
184
+ .map((candidate) => path.join(resolved, candidate))
185
+ .find((candidate) => fs.existsSync(candidate));
186
+ if (indexFile && isExtensionFile(indexFile)) {
187
+ addCandidate({
188
+ candidates: params.candidates,
189
+ seen: params.seen,
190
+ idHint: path.basename(resolved),
191
+ source: indexFile,
192
+ origin: params.origin,
193
+ workspaceDir: params.workspaceDir,
194
+ });
195
+ return;
196
+ }
197
+ discoverInDirectory({
198
+ dir: resolved,
199
+ origin: params.origin,
200
+ workspaceDir: params.workspaceDir,
201
+ candidates: params.candidates,
202
+ diagnostics: params.diagnostics,
203
+ seen: params.seen,
204
+ });
205
+ return;
206
+ }
207
+ }
208
+ export function discoverNexusPlugins(params) {
209
+ const candidates = [];
210
+ const diagnostics = [];
211
+ const seen = new Set();
212
+ const globalDir = path.join(CONFIG_DIR, "extensions");
213
+ discoverInDirectory({
214
+ dir: globalDir,
215
+ origin: "global",
216
+ candidates,
217
+ diagnostics,
218
+ seen,
219
+ });
220
+ const workspaceDir = params.workspaceDir?.trim();
221
+ if (workspaceDir) {
222
+ const workspaceRoot = resolveUserPath(workspaceDir);
223
+ // Try .nexus/extensions first, fall back to .clawdbot/extensions
224
+ const nexusExt = path.join(workspaceRoot, ".nexus", "extensions");
225
+ const clawdbotExt = path.join(workspaceRoot, ".clawdbot", "extensions");
226
+ const workspaceExt = fs.existsSync(nexusExt) ? nexusExt : clawdbotExt;
227
+ discoverInDirectory({
228
+ dir: workspaceExt,
229
+ origin: "workspace",
230
+ workspaceDir: workspaceRoot,
231
+ candidates,
232
+ diagnostics,
233
+ seen,
234
+ });
235
+ }
236
+ const extra = params.extraPaths ?? [];
237
+ for (const extraPath of extra) {
238
+ if (typeof extraPath !== "string")
239
+ continue;
240
+ const trimmed = extraPath.trim();
241
+ if (!trimmed)
242
+ continue;
243
+ discoverFromPath({
244
+ rawPath: trimmed,
245
+ origin: "config",
246
+ workspaceDir: workspaceDir?.trim() || undefined,
247
+ candidates,
248
+ diagnostics,
249
+ seen,
250
+ });
251
+ }
252
+ return { candidates, diagnostics };
253
+ }