ai-worklens-agent 0.1.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.
- package/README.md +166 -0
- package/package.json +22 -0
- package/src/cli.mjs +159 -0
- package/src/config.mjs +83 -0
- package/src/event-builder.mjs +77 -0
- package/src/hook-adapter.mjs +351 -0
- package/src/hook-smoke.mjs +105 -0
- package/src/install.mjs +602 -0
- package/src/mcp-server.mjs +364 -0
- package/src/publish-npm.mjs +149 -0
- package/src/queue.mjs +128 -0
- package/src/team-rollout.mjs +197 -0
- package/src/uploader.mjs +428 -0
package/src/install.mjs
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { writeClientConfig } from "./config.mjs";
|
|
7
|
+
import { getToolProfile, listToolProfiles, normalizeToolId } from "../../collector-protocol/src/tool-profiles.mjs";
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const projectRoot = path.resolve(__dirname, "../../..");
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const result = {};
|
|
14
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
15
|
+
const arg = argv[index];
|
|
16
|
+
if (!arg.startsWith("--")) continue;
|
|
17
|
+
const key = arg.slice(2);
|
|
18
|
+
const next = argv[index + 1];
|
|
19
|
+
if (!next || next.startsWith("--")) result[key] = true;
|
|
20
|
+
else {
|
|
21
|
+
result[key] = next;
|
|
22
|
+
index += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildInstallManifest(targetDir, options = {}) {
|
|
29
|
+
const node = process.execPath;
|
|
30
|
+
const selectedTool = normalizeToolId(options.tool || "codex");
|
|
31
|
+
const profile = getToolProfile(selectedTool);
|
|
32
|
+
const displayTargetDir = displayPath(targetDir);
|
|
33
|
+
const configFile = path.join(displayTargetDir, "client.json");
|
|
34
|
+
const mcpCommand = node;
|
|
35
|
+
const mcpArgs = [path.join(projectRoot, "packages/client-agent/src/mcp-server.mjs")];
|
|
36
|
+
const hookCommand = node;
|
|
37
|
+
const hookArgs = [path.join(projectRoot, "packages/client-agent/src/hook-adapter.mjs"), "--config", configFile, "--tool", selectedTool];
|
|
38
|
+
const artifacts = buildToolArtifacts({
|
|
39
|
+
targetDir,
|
|
40
|
+
displayTargetDir,
|
|
41
|
+
selectedTool,
|
|
42
|
+
configFile,
|
|
43
|
+
mcpCommand,
|
|
44
|
+
mcpArgs,
|
|
45
|
+
hookCommand,
|
|
46
|
+
hookArgs
|
|
47
|
+
});
|
|
48
|
+
const selectedArtifact = artifacts[selectedTool];
|
|
49
|
+
return {
|
|
50
|
+
version: 1,
|
|
51
|
+
targetDir: displayTargetDir,
|
|
52
|
+
selectedTool,
|
|
53
|
+
supportedTools: listToolProfiles(),
|
|
54
|
+
mcp: {
|
|
55
|
+
name: profile.mcpServerName,
|
|
56
|
+
command: mcpCommand,
|
|
57
|
+
args: mcpArgs,
|
|
58
|
+
env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: selectedTool }
|
|
59
|
+
},
|
|
60
|
+
hook: {
|
|
61
|
+
name: profile.hookName,
|
|
62
|
+
command: hookCommand,
|
|
63
|
+
args: hookArgs
|
|
64
|
+
},
|
|
65
|
+
eventCli: {
|
|
66
|
+
command: node,
|
|
67
|
+
args: [path.join(projectRoot, "packages/client-agent/src/cli.mjs"), "--config", configFile]
|
|
68
|
+
},
|
|
69
|
+
generatedFiles: {
|
|
70
|
+
clientConfig: configFile,
|
|
71
|
+
installManifest: path.join(targetDir, "install-manifest.json"),
|
|
72
|
+
...Object.fromEntries(Object.entries(artifacts).flatMap(([toolId, artifact]) => [
|
|
73
|
+
[`${camelKey(toolId)}McpConfig`, artifact.configFile],
|
|
74
|
+
[`${camelKey(toolId)}HookScript`, artifact.hookFile],
|
|
75
|
+
...Object.entries(artifact.extraFiles || {}).map(([key, value]) => [`${camelKey(toolId)}${upperFirst(key)}`, value.file])
|
|
76
|
+
])),
|
|
77
|
+
codexMcpSnippet: artifacts.codex.configFile,
|
|
78
|
+
checkinScript: path.join(targetDir, "worklens-checkin.sh"),
|
|
79
|
+
selfCheckScript: path.join(targetDir, "worklens-self-check.sh"),
|
|
80
|
+
autoUpdateScript: path.join(targetDir, "worklens-auto-update.sh"),
|
|
81
|
+
registerAutoUpdateScript: path.join(targetDir, "worklens-register-autoupdate.sh"),
|
|
82
|
+
unregisterAutoUpdateScript: path.join(targetDir, "worklens-unregister-autoupdate.sh"),
|
|
83
|
+
windowsRegisterAutoUpdateScript: path.join(targetDir, "worklens-register-autoupdate.ps1"),
|
|
84
|
+
installOrUpdateScript: path.join(targetDir, "worklens-install-or-update.sh"),
|
|
85
|
+
readme: path.join(targetDir, "README.md")
|
|
86
|
+
},
|
|
87
|
+
toolArtifacts: artifacts,
|
|
88
|
+
selectedConfig: selectedArtifact.config,
|
|
89
|
+
codexConfigToml: artifacts.codex.config,
|
|
90
|
+
claudeCodeMcpJson: artifacts["claude-code"].config,
|
|
91
|
+
opencodeConfigJson: artifacts.opencode.config,
|
|
92
|
+
privacy: {
|
|
93
|
+
rawPromptUploaded: false,
|
|
94
|
+
fullReplyUploaded: false,
|
|
95
|
+
localRedaction: true
|
|
96
|
+
},
|
|
97
|
+
serverUrl: options.serverUrl || "http://127.0.0.1:8797"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function installClient(options = {}) {
|
|
102
|
+
const targetDir = options.targetDir || path.join(os.homedir(), ".ai-worklens");
|
|
103
|
+
fs.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
|
104
|
+
const configFile = path.join(targetDir, "client.json");
|
|
105
|
+
const manifestFile = path.join(targetDir, "install-manifest.json");
|
|
106
|
+
const mcpSnippetFile = path.join(targetDir, "codex-mcp-snippet.toml");
|
|
107
|
+
const checkinScriptFile = path.join(targetDir, "worklens-checkin.sh");
|
|
108
|
+
const selfCheckScriptFile = path.join(targetDir, "worklens-self-check.sh");
|
|
109
|
+
const autoUpdateScriptFile = path.join(targetDir, "worklens-auto-update.sh");
|
|
110
|
+
const registerAutoUpdateScriptFile = path.join(targetDir, "worklens-register-autoupdate.sh");
|
|
111
|
+
const unregisterAutoUpdateScriptFile = path.join(targetDir, "worklens-unregister-autoupdate.sh");
|
|
112
|
+
const windowsRegisterAutoUpdateScriptFile = path.join(targetDir, "worklens-register-autoupdate.ps1");
|
|
113
|
+
const installOrUpdateScriptFile = path.join(targetDir, "worklens-install-or-update.sh");
|
|
114
|
+
const readmeFile = path.join(targetDir, "README.md");
|
|
115
|
+
const selectedTool = normalizeToolId(options.tool || "codex");
|
|
116
|
+
writeClientConfig(configFile, {
|
|
117
|
+
serverUrl: options.serverUrl || "http://127.0.0.1:8797",
|
|
118
|
+
collectorToken: options.collectorToken || "",
|
|
119
|
+
clientId: options.clientId || "",
|
|
120
|
+
clientVersion: options.clientVersion || "0.1.0",
|
|
121
|
+
tool: selectedTool,
|
|
122
|
+
model: {
|
|
123
|
+
provider: options.modelProvider || "",
|
|
124
|
+
name: options.modelName || "",
|
|
125
|
+
version: options.modelVersion || "",
|
|
126
|
+
family: options.modelFamily || ""
|
|
127
|
+
},
|
|
128
|
+
employee: {
|
|
129
|
+
id: options.employeeId || "",
|
|
130
|
+
name: options.employeeName || "",
|
|
131
|
+
pinyinName: options.employeePinyin || options.pinyinName || "",
|
|
132
|
+
department: options.department || "",
|
|
133
|
+
role: options.role || ""
|
|
134
|
+
},
|
|
135
|
+
upload: options.upload || { timeoutMs: 5000, batchSize: 50 },
|
|
136
|
+
collection: options.collection || {},
|
|
137
|
+
update: options.updatePolicy || {}
|
|
138
|
+
});
|
|
139
|
+
const manifest = buildInstallManifest(targetDir, options);
|
|
140
|
+
fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
141
|
+
fs.chmodSync(manifestFile, 0o600);
|
|
142
|
+
for (const artifact of Object.values(manifest.toolArtifacts)) {
|
|
143
|
+
fs.writeFileSync(artifact.configFile, `${artifact.config}\n`, { mode: 0o600 });
|
|
144
|
+
fs.chmodSync(artifact.configFile, 0o600);
|
|
145
|
+
fs.writeFileSync(artifact.hookFile, buildHookScript(artifact.hook.command, artifact.hook.args), { mode: 0o700 });
|
|
146
|
+
fs.chmodSync(artifact.hookFile, 0o700);
|
|
147
|
+
for (const file of Object.values(artifact.extraFiles || {})) {
|
|
148
|
+
fs.writeFileSync(file.file, `${file.content}\n`, { mode: file.mode || 0o600 });
|
|
149
|
+
fs.chmodSync(file.file, file.mode || 0o600);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!fs.existsSync(mcpSnippetFile)) fs.writeFileSync(mcpSnippetFile, `${manifest.codexConfigToml}\n`, { mode: 0o600 });
|
|
153
|
+
fs.writeFileSync(checkinScriptFile, buildCheckinScript(manifest.eventCli.command, manifest.eventCli.args), { mode: 0o700 });
|
|
154
|
+
fs.chmodSync(checkinScriptFile, 0o700);
|
|
155
|
+
fs.writeFileSync(selfCheckScriptFile, buildSelfCheckScript(manifest.eventCli.command, manifest.eventCli.args), { mode: 0o700 });
|
|
156
|
+
fs.chmodSync(selfCheckScriptFile, 0o700);
|
|
157
|
+
fs.writeFileSync(autoUpdateScriptFile, buildAutoUpdateScript(manifest.eventCli.command, manifest.eventCli.args), { mode: 0o700 });
|
|
158
|
+
fs.chmodSync(autoUpdateScriptFile, 0o700);
|
|
159
|
+
fs.writeFileSync(registerAutoUpdateScriptFile, buildRegisterAutoUpdateScript(targetDir), { mode: 0o700 });
|
|
160
|
+
fs.chmodSync(registerAutoUpdateScriptFile, 0o700);
|
|
161
|
+
fs.writeFileSync(unregisterAutoUpdateScriptFile, buildUnregisterAutoUpdateScript(), { mode: 0o700 });
|
|
162
|
+
fs.chmodSync(unregisterAutoUpdateScriptFile, 0o700);
|
|
163
|
+
fs.writeFileSync(windowsRegisterAutoUpdateScriptFile, buildWindowsRegisterAutoUpdateScript(targetDir), { mode: 0o600 });
|
|
164
|
+
fs.chmodSync(windowsRegisterAutoUpdateScriptFile, 0o600);
|
|
165
|
+
fs.writeFileSync(installOrUpdateScriptFile, buildInstallOrUpdateScript(manifest.eventCli.command, manifest.eventCli.args, targetDir), { mode: 0o700 });
|
|
166
|
+
fs.chmodSync(installOrUpdateScriptFile, 0o700);
|
|
167
|
+
fs.writeFileSync(readmeFile, buildReadme(manifest), { mode: 0o600 });
|
|
168
|
+
fs.chmodSync(readmeFile, 0o600);
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
targetDir,
|
|
172
|
+
configFile,
|
|
173
|
+
manifestFile,
|
|
174
|
+
generatedFiles: manifest.generatedFiles,
|
|
175
|
+
manifest
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function tomlString(value) {
|
|
180
|
+
return JSON.stringify(String(value));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function displayPath(value) {
|
|
184
|
+
const input = String(value || "");
|
|
185
|
+
if (input === "~") return "$HOME";
|
|
186
|
+
if (input.startsWith("~/")) return `$HOME/${input.slice(2)}`;
|
|
187
|
+
return input;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function tomlArray(values) {
|
|
191
|
+
return `[${values.map((value) => tomlString(value)).join(", ")}]`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function camelKey(value) {
|
|
195
|
+
return String(value).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function upperFirst(value) {
|
|
199
|
+
const input = String(value);
|
|
200
|
+
return input ? `${input[0].toUpperCase()}${input.slice(1)}` : input;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildToolArtifacts({ targetDir, displayTargetDir, configFile, mcpCommand, mcpArgs }) {
|
|
204
|
+
return Object.fromEntries(listToolProfiles().map((profile) => {
|
|
205
|
+
const hookArgs = [path.join(projectRoot, "packages/client-agent/src/hook-adapter.mjs"), "--config", configFile, "--tool", profile.id];
|
|
206
|
+
const artifact = {
|
|
207
|
+
profile,
|
|
208
|
+
configFile: path.join(targetDir, profile.configFileName),
|
|
209
|
+
hookFile: path.join(targetDir, profile.hookFileName),
|
|
210
|
+
hook: {
|
|
211
|
+
name: profile.hookName,
|
|
212
|
+
command: mcpCommand,
|
|
213
|
+
args: hookArgs
|
|
214
|
+
},
|
|
215
|
+
mcp: {
|
|
216
|
+
name: profile.mcpServerName,
|
|
217
|
+
command: mcpCommand,
|
|
218
|
+
args: mcpArgs,
|
|
219
|
+
env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id }
|
|
220
|
+
},
|
|
221
|
+
config: toolConfigFor(profile, {
|
|
222
|
+
configFile,
|
|
223
|
+
mcpCommand,
|
|
224
|
+
mcpArgs,
|
|
225
|
+
hookCommand: mcpCommand,
|
|
226
|
+
hookArgs,
|
|
227
|
+
displayTargetDir
|
|
228
|
+
}),
|
|
229
|
+
extraFiles: extraFilesFor(profile, {
|
|
230
|
+
targetDir,
|
|
231
|
+
configFile,
|
|
232
|
+
mcpCommand,
|
|
233
|
+
mcpArgs,
|
|
234
|
+
hookCommand: mcpCommand,
|
|
235
|
+
hookArgs
|
|
236
|
+
}),
|
|
237
|
+
note: profile.installNote
|
|
238
|
+
};
|
|
239
|
+
return [profile.id, artifact];
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function toolConfigFor(profile, { configFile, mcpCommand, mcpArgs, hookCommand, hookArgs }) {
|
|
244
|
+
if (profile.id === "codex") {
|
|
245
|
+
return [
|
|
246
|
+
`[mcp_servers.${profile.mcpServerName}]`,
|
|
247
|
+
`command = ${tomlString(mcpCommand)}`,
|
|
248
|
+
`args = ${tomlArray(mcpArgs)}`,
|
|
249
|
+
"",
|
|
250
|
+
`[mcp_servers.${profile.mcpServerName}.env]`,
|
|
251
|
+
`WORKLENS_CONFIG_FILE = ${tomlString(configFile)}`,
|
|
252
|
+
`WORKLENS_TOOL = ${tomlString(profile.id)}`
|
|
253
|
+
].join("\n");
|
|
254
|
+
}
|
|
255
|
+
if (profile.id === "claude-code") {
|
|
256
|
+
return JSON.stringify({
|
|
257
|
+
mcpServers: {
|
|
258
|
+
[profile.mcpServerName]: {
|
|
259
|
+
command: mcpCommand,
|
|
260
|
+
args: mcpArgs,
|
|
261
|
+
env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}, null, 2);
|
|
265
|
+
}
|
|
266
|
+
if (profile.id === "opencode") {
|
|
267
|
+
return JSON.stringify({
|
|
268
|
+
$schema: "https://opencode.ai/config.json",
|
|
269
|
+
mcp: {
|
|
270
|
+
[profile.mcpServerName]: {
|
|
271
|
+
type: "local",
|
|
272
|
+
command: [mcpCommand, ...mcpArgs],
|
|
273
|
+
enabled: true,
|
|
274
|
+
environment: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}, null, 2);
|
|
278
|
+
}
|
|
279
|
+
return JSON.stringify({ command: mcpCommand, args: mcpArgs, env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id } }, null, 2);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function extraFilesFor(profile, { targetDir, hookCommand, hookArgs }) {
|
|
283
|
+
if (profile.id === "claude-code") {
|
|
284
|
+
return {
|
|
285
|
+
hooksSettings: {
|
|
286
|
+
file: path.join(targetDir, "claude-code-hooks-settings.json"),
|
|
287
|
+
content: buildClaudeHooksSettings(hookCommand, hookArgs)
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (profile.id === "opencode") {
|
|
292
|
+
return {
|
|
293
|
+
plugin: {
|
|
294
|
+
file: path.join(targetDir, "opencode-ai-worklens-plugin.js"),
|
|
295
|
+
content: buildOpenCodePlugin(hookCommand, hookArgs),
|
|
296
|
+
mode: 0o600
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildClaudeHooksSettings(command, baseArgs) {
|
|
304
|
+
const events = [
|
|
305
|
+
"SessionStart",
|
|
306
|
+
"ModeChange",
|
|
307
|
+
"UserPromptSubmit",
|
|
308
|
+
"UserPromptExpansion",
|
|
309
|
+
"PreToolUse",
|
|
310
|
+
"PermissionRequest",
|
|
311
|
+
"PermissionDenied",
|
|
312
|
+
"PostToolUse",
|
|
313
|
+
"PostToolUseFailure",
|
|
314
|
+
"PostToolBatch",
|
|
315
|
+
"TaskCreated",
|
|
316
|
+
"TaskCompleted",
|
|
317
|
+
"SubagentStart",
|
|
318
|
+
"SubagentStop",
|
|
319
|
+
"FileChanged",
|
|
320
|
+
"ConfigChange",
|
|
321
|
+
"CwdChanged",
|
|
322
|
+
"Notification",
|
|
323
|
+
"PreCompact",
|
|
324
|
+
"PostCompact",
|
|
325
|
+
"Stop",
|
|
326
|
+
"StopFailure",
|
|
327
|
+
"SessionEnd"
|
|
328
|
+
];
|
|
329
|
+
const matcherEvents = new Set(["PreToolUse", "PermissionRequest", "PermissionDenied", "PostToolUse", "PostToolUseFailure", "PostToolBatch"]);
|
|
330
|
+
const hooks = Object.fromEntries(events.map((eventName) => {
|
|
331
|
+
const entry = {
|
|
332
|
+
hooks: [
|
|
333
|
+
{
|
|
334
|
+
type: "command",
|
|
335
|
+
command,
|
|
336
|
+
args: [...baseArgs, "--event", eventName],
|
|
337
|
+
timeout: 10
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
};
|
|
341
|
+
if (matcherEvents.has(eventName)) entry.matcher = "*";
|
|
342
|
+
return [eventName, [entry]];
|
|
343
|
+
}));
|
|
344
|
+
return JSON.stringify({
|
|
345
|
+
description: "AI WorkLens hooks for AI usage analytics",
|
|
346
|
+
hooks
|
|
347
|
+
}, null, 2);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildOpenCodePlugin(command, baseArgs) {
|
|
351
|
+
const events = [
|
|
352
|
+
"command.executed",
|
|
353
|
+
"file.edited",
|
|
354
|
+
"file.watcher.updated",
|
|
355
|
+
"installation.updated",
|
|
356
|
+
"lsp.client.diagnostics",
|
|
357
|
+
"lsp.updated",
|
|
358
|
+
"message.part.removed",
|
|
359
|
+
"message.part.updated",
|
|
360
|
+
"message.removed",
|
|
361
|
+
"message.updated",
|
|
362
|
+
"permission.asked",
|
|
363
|
+
"permission.replied",
|
|
364
|
+
"server.connected",
|
|
365
|
+
"session.created",
|
|
366
|
+
"session.compacted",
|
|
367
|
+
"session.deleted",
|
|
368
|
+
"session.diff",
|
|
369
|
+
"session.error",
|
|
370
|
+
"session.idle",
|
|
371
|
+
"session.status",
|
|
372
|
+
"session.updated",
|
|
373
|
+
"todo.updated",
|
|
374
|
+
"shell.env",
|
|
375
|
+
"tool.execute.before",
|
|
376
|
+
"tool.execute.after",
|
|
377
|
+
"tui.prompt.append",
|
|
378
|
+
"tui.command.execute",
|
|
379
|
+
"tui.toast.show"
|
|
380
|
+
];
|
|
381
|
+
const commandLiteral = JSON.stringify(command);
|
|
382
|
+
const argsLiteral = JSON.stringify(baseArgs);
|
|
383
|
+
return [
|
|
384
|
+
"import { spawnSync } from \"node:child_process\";",
|
|
385
|
+
"",
|
|
386
|
+
`const command = ${commandLiteral};`,
|
|
387
|
+
`const baseArgs = ${argsLiteral};`,
|
|
388
|
+
"",
|
|
389
|
+
"function safeJson(value) {",
|
|
390
|
+
" const seen = new WeakSet();",
|
|
391
|
+
" return JSON.stringify(value, (key, item) => {",
|
|
392
|
+
" if (typeof item === \"object\" && item !== null) {",
|
|
393
|
+
" if (seen.has(item)) return \"[Circular]\";",
|
|
394
|
+
" seen.add(item);",
|
|
395
|
+
" }",
|
|
396
|
+
" return item;",
|
|
397
|
+
" });",
|
|
398
|
+
"}",
|
|
399
|
+
"",
|
|
400
|
+
"function record(eventName, input, output) {",
|
|
401
|
+
" const payload = { event: eventName, hook_event_name: eventName, input, output };",
|
|
402
|
+
" spawnSync(command, [...baseArgs, \"--event\", eventName], {",
|
|
403
|
+
" input: safeJson(payload),",
|
|
404
|
+
" encoding: \"utf8\",",
|
|
405
|
+
" stdio: [\"pipe\", \"ignore\", \"ignore\"],",
|
|
406
|
+
" timeout: 5000",
|
|
407
|
+
" });",
|
|
408
|
+
"}",
|
|
409
|
+
"",
|
|
410
|
+
"export const AIWorkLensPlugin = async () => ({",
|
|
411
|
+
...events.map((eventName) => ` ${JSON.stringify(eventName)}: async (input, output) => record(${JSON.stringify(eventName)}, input, output),`),
|
|
412
|
+
"});"
|
|
413
|
+
].join("\n");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function shellQuote(value) {
|
|
417
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function buildHookScript(command, args) {
|
|
421
|
+
return [
|
|
422
|
+
"#!/usr/bin/env sh",
|
|
423
|
+
"set -eu",
|
|
424
|
+
`exec ${shellQuote(command)} ${args.map(shellQuote).join(" ")}`
|
|
425
|
+
].join("\n") + "\n";
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function buildCheckinScript(command, args) {
|
|
429
|
+
return [
|
|
430
|
+
"#!/usr/bin/env sh",
|
|
431
|
+
"set -eu",
|
|
432
|
+
`${shellQuote(command)} ${args.map(shellQuote).join(" ")} sync-config || true`,
|
|
433
|
+
`${shellQuote(command)} ${args.map(shellQuote).join(" ")} recover --sync false --checkin false || true`,
|
|
434
|
+
`exec ${shellQuote(command)} ${args.map(shellQuote).join(" ")} checkin`
|
|
435
|
+
].join("\n") + "\n";
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function buildSelfCheckScript(command, args) {
|
|
439
|
+
return [
|
|
440
|
+
"#!/usr/bin/env sh",
|
|
441
|
+
"set -eu",
|
|
442
|
+
`exec ${shellQuote(command)} ${args.map(shellQuote).join(" ")} doctor`
|
|
443
|
+
].join("\n") + "\n";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function buildAutoUpdateScript(command, args) {
|
|
447
|
+
return [
|
|
448
|
+
"#!/usr/bin/env sh",
|
|
449
|
+
"set -eu",
|
|
450
|
+
`${shellQuote(command)} ${args.map(shellQuote).join(" ")} recover --checkin false || true`,
|
|
451
|
+
`exec ${shellQuote(command)} ${args.map(shellQuote).join(" ")} auto-update`
|
|
452
|
+
].join("\n") + "\n";
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function buildRegisterAutoUpdateScript(targetDir) {
|
|
456
|
+
const plist = "com.ai-worklens.autoupdate.plist";
|
|
457
|
+
const interval = 1800;
|
|
458
|
+
return [
|
|
459
|
+
"#!/usr/bin/env sh",
|
|
460
|
+
"set -eu",
|
|
461
|
+
"if [ \"$(uname -s 2>/dev/null || echo unknown)\" != \"Darwin\" ]; then",
|
|
462
|
+
" echo \"background autoupdate registration is currently automatic on macOS only\"",
|
|
463
|
+
" exit 0",
|
|
464
|
+
"fi",
|
|
465
|
+
"LABEL=\"com.ai-worklens.autoupdate\"",
|
|
466
|
+
"LAUNCH_DIR=\"$HOME/Library/LaunchAgents\"",
|
|
467
|
+
"LOG_DIR=\"$HOME/.ai-worklens/logs\"",
|
|
468
|
+
`PLIST="$LAUNCH_DIR/${plist}"`,
|
|
469
|
+
`AUTO_UPDATE=${shellQuote(path.join(targetDir, "worklens-auto-update.sh"))}`,
|
|
470
|
+
"mkdir -p \"$LAUNCH_DIR\" \"$LOG_DIR\"",
|
|
471
|
+
"cat > \"$PLIST.tmp\" <<EOF",
|
|
472
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
473
|
+
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">",
|
|
474
|
+
"<plist version=\"1.0\">",
|
|
475
|
+
"<dict>",
|
|
476
|
+
" <key>Label</key>",
|
|
477
|
+
" <string>com.ai-worklens.autoupdate</string>",
|
|
478
|
+
" <key>ProgramArguments</key>",
|
|
479
|
+
" <array>",
|
|
480
|
+
" <string>$AUTO_UPDATE</string>",
|
|
481
|
+
" </array>",
|
|
482
|
+
" <key>RunAtLoad</key>",
|
|
483
|
+
" <true/>",
|
|
484
|
+
" <key>StartInterval</key>",
|
|
485
|
+
` <integer>${interval}</integer>`,
|
|
486
|
+
" <key>StandardOutPath</key>",
|
|
487
|
+
" <string>$LOG_DIR/autoupdate.log</string>",
|
|
488
|
+
" <key>StandardErrorPath</key>",
|
|
489
|
+
" <string>$LOG_DIR/autoupdate.err.log</string>",
|
|
490
|
+
"</dict>",
|
|
491
|
+
"</plist>",
|
|
492
|
+
"EOF",
|
|
493
|
+
"mv \"$PLIST.tmp\" \"$PLIST\"",
|
|
494
|
+
"if command -v launchctl >/dev/null 2>&1; then",
|
|
495
|
+
" launchctl bootout \"gui/$(id -u)\" \"$PLIST\" >/dev/null 2>&1 || true",
|
|
496
|
+
" launchctl bootstrap \"gui/$(id -u)\" \"$PLIST\" >/dev/null 2>&1 || true",
|
|
497
|
+
" launchctl kickstart -k \"gui/$(id -u)/$LABEL\" >/dev/null 2>&1 || true",
|
|
498
|
+
"fi",
|
|
499
|
+
"echo \"registered $LABEL\""
|
|
500
|
+
].join("\n") + "\n";
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function buildUnregisterAutoUpdateScript() {
|
|
504
|
+
return [
|
|
505
|
+
"#!/usr/bin/env sh",
|
|
506
|
+
"set -eu",
|
|
507
|
+
"LABEL=\"com.ai-worklens.autoupdate\"",
|
|
508
|
+
"PLIST=\"$HOME/Library/LaunchAgents/$LABEL.plist\"",
|
|
509
|
+
"if command -v launchctl >/dev/null 2>&1; then",
|
|
510
|
+
" launchctl bootout \"gui/$(id -u)\" \"$PLIST\" >/dev/null 2>&1 || true",
|
|
511
|
+
"fi",
|
|
512
|
+
"rm -f \"$PLIST\"",
|
|
513
|
+
"echo \"unregistered $LABEL\""
|
|
514
|
+
].join("\n") + "\n";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function buildWindowsRegisterAutoUpdateScript(targetDir) {
|
|
518
|
+
const autoUpdate = path.win32.join("%USERPROFILE%", ".ai-worklens", "worklens-auto-update.sh");
|
|
519
|
+
return [
|
|
520
|
+
"$TaskName = \"AIWorkLensAutoUpdate\"",
|
|
521
|
+
`$AutoUpdate = \"${autoUpdate}\"`,
|
|
522
|
+
"$Action = New-ScheduledTaskAction -Execute \"wsl.exe\" -Argument \"sh $AutoUpdate\"",
|
|
523
|
+
"$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(5) -RepetitionInterval (New-TimeSpan -Hours 6)",
|
|
524
|
+
"$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries",
|
|
525
|
+
"Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Force",
|
|
526
|
+
`Write-Output \"Registered $TaskName for ${targetDir}\"`
|
|
527
|
+
].join("\r\n") + "\r\n";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function buildInstallOrUpdateScript(command, args, targetDir) {
|
|
531
|
+
return [
|
|
532
|
+
"#!/usr/bin/env sh",
|
|
533
|
+
"set -eu",
|
|
534
|
+
`${shellQuote(command)} ${args.map(shellQuote).join(" ")} sync-config || true`,
|
|
535
|
+
`${shellQuote(command)} ${args.map(shellQuote).join(" ")} recover --sync false --checkin false || true`,
|
|
536
|
+
`${shellQuote(command)} ${args.map(shellQuote).join(" ")} auto-update || true`,
|
|
537
|
+
`${shellQuote(command)} ${args.map(shellQuote).join(" ")} checkin || true`,
|
|
538
|
+
`if [ -x ${shellQuote(path.join(targetDir, "worklens-register-autoupdate.sh"))} ]; then`,
|
|
539
|
+
` ${shellQuote(path.join(targetDir, "worklens-register-autoupdate.sh"))} >/dev/null 2>&1 || true`,
|
|
540
|
+
"fi",
|
|
541
|
+
`exec ${shellQuote(command)} ${args.map(shellQuote).join(" ")} doctor`
|
|
542
|
+
].join("\n") + "\n";
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function buildReadme(manifest) {
|
|
546
|
+
const toolSections = Object.values(manifest.toolArtifacts).flatMap((artifact) => [
|
|
547
|
+
`## ${artifact.profile.label}`,
|
|
548
|
+
"",
|
|
549
|
+
artifact.note,
|
|
550
|
+
"",
|
|
551
|
+
"```",
|
|
552
|
+
artifact.config,
|
|
553
|
+
"```",
|
|
554
|
+
""
|
|
555
|
+
]);
|
|
556
|
+
return [
|
|
557
|
+
"# AI WorkLens Client",
|
|
558
|
+
"",
|
|
559
|
+
"本目录由员工端安装器生成,包含本机采集配置、MCP 配置片段、Hook 脚本和健康上报脚本。",
|
|
560
|
+
"",
|
|
561
|
+
"## 文件",
|
|
562
|
+
"",
|
|
563
|
+
`- client.json: 员工端配置和中心端下发规则。`,
|
|
564
|
+
`- install-manifest.json: 安装清单。`,
|
|
565
|
+
`- *-mcp.*: 各 AI 工具的 MCP 配置片段。`,
|
|
566
|
+
`- *-hook.sh: 各 AI 工具的 hook adapter 启动脚本。`,
|
|
567
|
+
`- claude-code-hooks-settings.json: Claude Code hooks 配置片段。`,
|
|
568
|
+
`- opencode-ai-worklens-plugin.js: OpenCode 本地插件。`,
|
|
569
|
+
`- worklens-checkin.sh: 同步规则、补传离线队列并上报健康状态。`,
|
|
570
|
+
`- worklens-self-check.sh: 检查中心端连通性、本地配置和离线队列。`,
|
|
571
|
+
`- worklens-auto-update.sh: 拉取中心端版本策略,并在中心端恢复后自动补传离线队列。`,
|
|
572
|
+
`- worklens-register-autoupdate.sh: 在 macOS 用户级 LaunchAgent 注册后台静默更新和恢复上报任务。`,
|
|
573
|
+
`- worklens-unregister-autoupdate.sh: 移除后台静默更新任务。`,
|
|
574
|
+
`- worklens-install-or-update.sh: 同步中心端规则、上报健康并执行自检。`,
|
|
575
|
+
"",
|
|
576
|
+
...toolSections,
|
|
577
|
+
"## 默认选中工具",
|
|
578
|
+
"",
|
|
579
|
+
`${manifest.selectedTool}`
|
|
580
|
+
].join("\n") + "\n";
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || "")) {
|
|
584
|
+
const args = parseArgs(process.argv.slice(2));
|
|
585
|
+
const result = installClient({
|
|
586
|
+
targetDir: args["target-dir"],
|
|
587
|
+
serverUrl: args["server-url"],
|
|
588
|
+
collectorToken: args["collector-token"],
|
|
589
|
+
employeeId: args["employee-id"],
|
|
590
|
+
employeeName: args["employee-name"],
|
|
591
|
+
employeePinyin: args["employee-pinyin"],
|
|
592
|
+
department: args.department,
|
|
593
|
+
role: args.role,
|
|
594
|
+
clientId: args["client-id"],
|
|
595
|
+
tool: args.tool,
|
|
596
|
+
modelProvider: args["model-provider"],
|
|
597
|
+
modelName: args["model-name"],
|
|
598
|
+
modelVersion: args["model-version"],
|
|
599
|
+
modelFamily: args["model-family"]
|
|
600
|
+
});
|
|
601
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
602
|
+
}
|