log-llm-config 1.5.15 → 1.5.18
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/dist/endpoint_client/compress_payload.js +25 -0
- package/dist/endpoint_client/http_transport.js +3 -1
- package/dist/endpoint_client/startup_api.js +3 -2
- package/dist/log_config_files/collection/directory_collector.js +10 -0
- package/dist/log_config_files/collection/plugin_collector.js +110 -9
- package/dist/log_config_files/collection/skill_symlink.js +191 -0
- package/dist/log_config_files/collection/skills_cli_collector.js +18 -2
- package/dist/log_config_files/runtime/prompt-collection-policy.js +57 -0
- package/dist/log_config_files/sender/batch_sender.js +14 -5
- package/dist/log_uuid/startup_sender.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
/** macOS ships gzip at a fixed path; call it directly so a modified PATH cannot redirect us. */
|
|
3
|
+
const GZIP_BIN = '/usr/bin/gzip';
|
|
4
|
+
const GZIP_TIMEOUT_MS = 10_000;
|
|
5
|
+
const GZIP_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* Compress a request body with the system gzip. Output is gzip-framed, so it pairs with
|
|
8
|
+
* `Content-Encoding: gzip`. Throws if gzip is missing, times out, or exits non-zero.
|
|
9
|
+
*/
|
|
10
|
+
export function gzipPayload(payload) {
|
|
11
|
+
const child = spawnSync(GZIP_BIN, ['-c'], {
|
|
12
|
+
input: Buffer.from(payload, 'utf8'),
|
|
13
|
+
timeout: GZIP_TIMEOUT_MS,
|
|
14
|
+
maxBuffer: GZIP_MAX_OUTPUT_BYTES,
|
|
15
|
+
});
|
|
16
|
+
if (child.error)
|
|
17
|
+
throw child.error;
|
|
18
|
+
if (child.status !== 0) {
|
|
19
|
+
const stderr = (child.stderr ?? '').toString().trim();
|
|
20
|
+
throw new Error(stderr || `${GZIP_BIN} exited ${child.status ?? 'unknown'}`);
|
|
21
|
+
}
|
|
22
|
+
if (!child.stdout || child.stdout.length === 0)
|
|
23
|
+
throw new Error(`${GZIP_BIN} produced no output`);
|
|
24
|
+
return child.stdout;
|
|
25
|
+
}
|
|
@@ -25,6 +25,8 @@ function buildBodyOptions(url, payload, method, timeoutMs) {
|
|
|
25
25
|
headers: {
|
|
26
26
|
'Content-Type': 'application/json',
|
|
27
27
|
'Content-Length': Buffer.byteLength(payload).toString(),
|
|
28
|
+
// A Buffer body is always gzip output from compress_payload.gzipPayload.
|
|
29
|
+
...(Buffer.isBuffer(payload) ? { 'Content-Encoding': 'gzip' } : {}),
|
|
28
30
|
},
|
|
29
31
|
timeout: timeoutMs,
|
|
30
32
|
};
|
|
@@ -46,7 +48,7 @@ export function executeGet(urlStr, timeoutMs) {
|
|
|
46
48
|
req.end();
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
|
-
/** Execute a POST/PATCH request. Rejects on network error or timeout. */
|
|
51
|
+
/** Execute a POST/PATCH request. A Buffer payload is sent as `Content-Encoding: gzip`. Rejects on network error or timeout. */
|
|
50
52
|
export function executeBody(urlStr, method, payload, timeoutMs) {
|
|
51
53
|
const url = new URL(urlStr);
|
|
52
54
|
const options = buildBodyOptions(url, payload, method, timeoutMs);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { executeBody } from './http_transport.js';
|
|
2
|
+
import { gzipPayload } from './compress_payload.js';
|
|
2
3
|
export const postStartupPayload = async (endpointUrl, body, timeoutMs = 5000) => {
|
|
3
|
-
const payload = JSON.stringify(body);
|
|
4
|
+
const payload = gzipPayload(JSON.stringify(body));
|
|
4
5
|
const { statusCode, statusMessage, body: responseBody } = await executeBody(endpointUrl, 'POST', payload, timeoutMs);
|
|
5
6
|
if (statusCode >= 400) {
|
|
6
7
|
const msg = statusCode === 413
|
|
@@ -18,7 +19,7 @@ export const postStartupPayload = async (endpointUrl, body, timeoutMs = 5000) =>
|
|
|
18
19
|
}
|
|
19
20
|
};
|
|
20
21
|
export const patchPayload = async (endpointUrl, body, timeoutMs = 10000) => {
|
|
21
|
-
const payload = JSON.stringify(body);
|
|
22
|
+
const payload = gzipPayload(JSON.stringify(body));
|
|
22
23
|
const { body: responseBody } = await executeBody(endpointUrl, 'PATCH', payload, timeoutMs);
|
|
23
24
|
if (!responseBody)
|
|
24
25
|
return { status: 'error', error: 'empty_response' };
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
4
|
+
import { collectSkillMdRecord, isSkillDirEntry } from './skill_symlink.js';
|
|
4
5
|
function collectSubdirMdFiles(dirPath, fileType, mdFilename, source) {
|
|
5
6
|
const results = [];
|
|
7
|
+
const isSkillMd = mdFilename.toLowerCase() === 'skill.md';
|
|
6
8
|
try {
|
|
7
9
|
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
10
|
+
if (isSkillMd) {
|
|
11
|
+
if (!isSkillDirEntry(entry))
|
|
12
|
+
continue;
|
|
13
|
+
const record = collectSkillMdRecord(join(dirPath, entry.name), fileType, source, mdFilename);
|
|
14
|
+
if (record)
|
|
15
|
+
results.push(record);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
8
18
|
if (!entry.isDirectory())
|
|
9
19
|
continue;
|
|
10
20
|
const mdPath = join(dirPath, entry.name, mdFilename);
|
|
@@ -1,20 +1,121 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { extname, join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
5
6
|
import { getInstalledPluginsPath, getPluginHooksPath, getPluginMcpPaths, getPluginSkillFilename, getPluginSkillsDir } from '../paths/path_constants_helpers.js';
|
|
6
7
|
import { versionFromPluginCachePath, parseInstalledPluginKey } from './plugin_version_helpers.js';
|
|
8
|
+
import { collectSkillMdRecord, isSkillDirEntry, symlinkInfoAllowsExtraFiles } from './skill_symlink.js';
|
|
9
|
+
/** Directories never worth shipping: git history is huge and already-compressed, deps are not the skill. */
|
|
10
|
+
const SKILL_SKIP_DIRS = new Set(['.git', 'node_modules']);
|
|
11
|
+
/** Fonts are the single largest thing in skill folders (canvas-design ships 54) and carry no signal. */
|
|
12
|
+
const SKILL_SKIP_EXTS = new Set(['.ttf', '.otf', '.woff', '.woff2']);
|
|
13
|
+
/** Inlined as text; everything else is recorded as path + size + hash. */
|
|
14
|
+
const SKILL_TEXT_EXTS = new Set([
|
|
15
|
+
'.md', '.txt', '.py', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.jsonc',
|
|
16
|
+
'.sh', '.bash', '.zsh', '.yaml', '.yml', '.xml', '.xsd', '.html', '.css', '.toml',
|
|
17
|
+
'.lock', '.csv', '.sql', '.rb', '.go', '.java', '.php', '.rs',
|
|
18
|
+
]);
|
|
19
|
+
const SKILL_MAX_FILE_BYTES = 1024 * 1024;
|
|
20
|
+
const SKILL_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
21
|
+
const SKILL_MAX_FILES = 500;
|
|
22
|
+
const SKILL_MAX_DEPTH = 8;
|
|
23
|
+
/** Hidden files are collected, but credential-shaped ones are recorded without their contents. */
|
|
24
|
+
function isSecretName(name) {
|
|
25
|
+
const lower = name.toLowerCase();
|
|
26
|
+
return lower === '.env' || lower.startsWith('.env.') || lower === '.npmrc' || lower === '.netrc'
|
|
27
|
+
|| lower === '.pgpass' || lower.endsWith('.pem') || lower.endsWith('.key');
|
|
28
|
+
}
|
|
29
|
+
function skillMetaRecord(filePath, size, sha256, ver) {
|
|
30
|
+
return {
|
|
31
|
+
file_type: 'claude_skill',
|
|
32
|
+
file_path: filePath,
|
|
33
|
+
collect_style: 'metadata',
|
|
34
|
+
raw_content: { source: 'file', size, ...(sha256 ? { sha256 } : {}), ...(ver ? { version: ver } : {}) },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function collectSkillDirFiles(dir, results, constants, skillFilename, budget, depth) {
|
|
38
|
+
if (depth > SKILL_MAX_DEPTH)
|
|
39
|
+
return;
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.warn(`Error reading skill dir ${dir}:`, err instanceof Error ? err.message : String(err));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const ent of entries) {
|
|
49
|
+
if (budget.files >= SKILL_MAX_FILES || budget.bytes >= SKILL_MAX_TOTAL_BYTES)
|
|
50
|
+
return;
|
|
51
|
+
const full = join(dir, ent.name);
|
|
52
|
+
if (ent.isDirectory()) {
|
|
53
|
+
if (!SKILL_SKIP_DIRS.has(ent.name))
|
|
54
|
+
collectSkillDirFiles(full, results, constants, skillFilename, budget, depth + 1);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ent.name === skillFilename)
|
|
58
|
+
continue;
|
|
59
|
+
// Extra skill files: skip nested symlinks so the walk stays inside this skill dir.
|
|
60
|
+
if (!ent.isFile())
|
|
61
|
+
continue;
|
|
62
|
+
const ext = extname(ent.name).toLowerCase();
|
|
63
|
+
if (SKILL_SKIP_EXTS.has(ext))
|
|
64
|
+
continue;
|
|
65
|
+
let size;
|
|
66
|
+
try {
|
|
67
|
+
size = statSync(full).size;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const ver = versionFromPluginCachePath(full, constants);
|
|
73
|
+
if (size > SKILL_MAX_FILE_BYTES) {
|
|
74
|
+
results.push(skillMetaRecord(full, size, '', ver));
|
|
75
|
+
budget.files += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!SKILL_TEXT_EXTS.has(ext) && ent.name !== skillFilename) {
|
|
79
|
+
let sha256 = '';
|
|
80
|
+
try {
|
|
81
|
+
sha256 = createHash('sha256').update(readFileSync(full)).digest('hex');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
results.push(skillMetaRecord(full, size, sha256, ver));
|
|
87
|
+
budget.files += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (isSecretName(ent.name)) {
|
|
91
|
+
results.push(skillMetaRecord(full, size, '', ver));
|
|
92
|
+
budget.files += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const content = readMarkdownFile(full);
|
|
96
|
+
if (content === null)
|
|
97
|
+
continue;
|
|
98
|
+
results.push({ file_type: 'claude_skill', file_path: full, raw_content: { content, source: 'file', ...(ver ? { version: ver } : {}) } });
|
|
99
|
+
budget.files += 1;
|
|
100
|
+
budget.bytes += Buffer.byteLength(content);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
7
103
|
function collectSkillFiles(skillsDir, results, constants) {
|
|
8
104
|
const skillFilename = getPluginSkillFilename(constants);
|
|
9
105
|
try {
|
|
10
|
-
for (const d of readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d
|
|
11
|
-
const
|
|
12
|
-
|
|
106
|
+
for (const d of readdirSync(skillsDir, { withFileTypes: true }).filter((d) => isSkillDirEntry(d))) {
|
|
107
|
+
const skillRoot = join(skillsDir, d.name);
|
|
108
|
+
const ver = versionFromPluginCachePath(join(skillRoot, skillFilename), constants);
|
|
109
|
+
const record = collectSkillMdRecord(skillRoot, 'claude_skill', 'file', skillFilename, ver ? { version: ver } : {});
|
|
110
|
+
if (!record)
|
|
13
111
|
continue;
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
112
|
+
results.push(record);
|
|
113
|
+
if (!symlinkInfoAllowsExtraFiles(record.symlink_info))
|
|
114
|
+
continue;
|
|
115
|
+
const budget = { files: 1, bytes: 0 };
|
|
116
|
+
collectSkillDirFiles(skillRoot, results, constants, skillFilename, budget, 0);
|
|
117
|
+
if (budget.files >= SKILL_MAX_FILES || budget.bytes >= SKILL_MAX_TOTAL_BYTES) {
|
|
118
|
+
console.warn(`Skill ${skillRoot} hit collection cap (${budget.files} files, ${budget.bytes} bytes)`);
|
|
18
119
|
}
|
|
19
120
|
}
|
|
20
121
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { accessSync, constants, lstatSync, readlinkSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { readMarkdownFile } from '../readers/file_readers.js';
|
|
4
|
+
function isMissingPathError(err) {
|
|
5
|
+
const code = err?.code;
|
|
6
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolve readlink's raw target the way the OS does when parent dirs include
|
|
10
|
+
* symlinks. A lexical join against the logical parent is wrong when that parent
|
|
11
|
+
* is itself a link (e.g. ~/.config → a dotfiles repo).
|
|
12
|
+
*/
|
|
13
|
+
export function resolveRawSymlinkTarget(linkPath, target) {
|
|
14
|
+
if (isAbsolute(target))
|
|
15
|
+
return resolve(target);
|
|
16
|
+
const physicalParent = realpathSync(dirname(linkPath));
|
|
17
|
+
return resolve(physicalParent, target);
|
|
18
|
+
}
|
|
19
|
+
function probeAccess(path) {
|
|
20
|
+
try {
|
|
21
|
+
accessSync(path, constants.F_OK);
|
|
22
|
+
return 'valid';
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return isMissingPathError(err) ? 'broken' : 'inaccessible';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readResolvedSymlinkTarget(linkPath) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = readlinkSync(linkPath);
|
|
31
|
+
const target = resolveRawSymlinkTarget(linkPath, raw);
|
|
32
|
+
return { target, status: probeAccess(target) };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err?.code === 'EINVAL') {
|
|
36
|
+
return { target: null, status: 'valid' };
|
|
37
|
+
}
|
|
38
|
+
return { target: null, status: isMissingPathError(err) ? 'broken' : 'inaccessible' };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function asOverall(status) {
|
|
42
|
+
return status === 'missing' ? 'broken' : status;
|
|
43
|
+
}
|
|
44
|
+
function worseStatus(current, next) {
|
|
45
|
+
const rank = { valid: 0, broken: 1, inaccessible: 2 };
|
|
46
|
+
return rank[next] > rank[current] ? next : current;
|
|
47
|
+
}
|
|
48
|
+
function emptyPath(path, status = 'valid') {
|
|
49
|
+
return { path, is_symlink: false, points_to: null, status };
|
|
50
|
+
}
|
|
51
|
+
/** True for a skills-dir child that is a real folder or a symlink slot. */
|
|
52
|
+
export function isSkillDirEntry(entry) {
|
|
53
|
+
return entry.isDirectory() || Boolean(entry.isSymbolicLink?.());
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Inspect one agent skill slot: the folder (local vs symlink) and SKILL.md inside it.
|
|
57
|
+
*/
|
|
58
|
+
export function inspectSkillLink(skillDir, skillFilename = 'SKILL.md') {
|
|
59
|
+
const skillMdPath = join(skillDir, skillFilename);
|
|
60
|
+
const skill_folder = emptyPath(skillDir);
|
|
61
|
+
const skill_md = emptyPath(skillMdPath);
|
|
62
|
+
try {
|
|
63
|
+
const dirStat = lstatSync(skillDir);
|
|
64
|
+
if (dirStat.isSymbolicLink()) {
|
|
65
|
+
skill_folder.is_symlink = true;
|
|
66
|
+
const resolved = readResolvedSymlinkTarget(skillDir);
|
|
67
|
+
skill_folder.points_to = resolved.target;
|
|
68
|
+
skill_folder.status = resolved.status;
|
|
69
|
+
}
|
|
70
|
+
else if (!dirStat.isDirectory()) {
|
|
71
|
+
skill_folder.status = 'broken';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
skill_folder.status = isMissingPathError(err) ? 'broken' : 'inaccessible';
|
|
76
|
+
skill_md.status = 'missing';
|
|
77
|
+
return {
|
|
78
|
+
status: asOverall(skill_folder.status),
|
|
79
|
+
skill_folder,
|
|
80
|
+
skill_md,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const mdStat = lstatSync(skillMdPath);
|
|
85
|
+
if (mdStat.isSymbolicLink()) {
|
|
86
|
+
skill_md.is_symlink = true;
|
|
87
|
+
const resolved = readResolvedSymlinkTarget(skillMdPath);
|
|
88
|
+
skill_md.points_to = resolved.target;
|
|
89
|
+
skill_md.status = resolved.status;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
skill_md.status = isMissingPathError(err) ? 'missing' : 'inaccessible';
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
status: worseStatus(asOverall(skill_folder.status), asOverall(skill_md.status)),
|
|
97
|
+
skill_folder,
|
|
98
|
+
skill_md,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function symlinkEntry(info) {
|
|
102
|
+
return {
|
|
103
|
+
path: info.path,
|
|
104
|
+
points_to: info.points_to,
|
|
105
|
+
status: info.status === 'missing' ? 'broken' : info.status,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Only include the path that is actually a symlink. No symlink → `{ symlink_enabled: false }`. */
|
|
109
|
+
export function skillLinkPayload(info) {
|
|
110
|
+
const symlink_info = { symlink_enabled: false };
|
|
111
|
+
if (info.skill_folder.is_symlink) {
|
|
112
|
+
symlink_info.symlink_enabled = true;
|
|
113
|
+
symlink_info.skill_folder = symlinkEntry(info.skill_folder);
|
|
114
|
+
}
|
|
115
|
+
if (info.skill_md.is_symlink) {
|
|
116
|
+
symlink_info.symlink_enabled = true;
|
|
117
|
+
symlink_info.skill_md = symlinkEntry(info.skill_md);
|
|
118
|
+
}
|
|
119
|
+
return symlink_info;
|
|
120
|
+
}
|
|
121
|
+
export function symlinkInfoAllowsExtraFiles(info) {
|
|
122
|
+
if (!info || typeof info !== 'object')
|
|
123
|
+
return true;
|
|
124
|
+
const payload = info;
|
|
125
|
+
if (!payload.symlink_enabled)
|
|
126
|
+
return true;
|
|
127
|
+
for (const entry of [payload.skill_folder, payload.skill_md]) {
|
|
128
|
+
if (entry?.status && entry.status !== 'valid')
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
function shouldEmitSkillSlot(info, skillFilename) {
|
|
134
|
+
if (info.skill_folder.is_symlink)
|
|
135
|
+
return true;
|
|
136
|
+
if (info.status === 'inaccessible')
|
|
137
|
+
return true;
|
|
138
|
+
if (info.skill_md.is_symlink)
|
|
139
|
+
return true;
|
|
140
|
+
try {
|
|
141
|
+
accessSync(join(info.skill_folder.path, skillFilename), constants.F_OK);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function unknownInspectFailure(skillDir, skillFilename, err) {
|
|
149
|
+
const status = isMissingPathError(err) ? 'broken' : 'inaccessible';
|
|
150
|
+
return {
|
|
151
|
+
status,
|
|
152
|
+
skill_folder: emptyPath(skillDir, status),
|
|
153
|
+
skill_md: emptyPath(join(skillDir, skillFilename), 'missing'),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Collect one SKILL.md inventory row, including symlink metadata even when the
|
|
158
|
+
* file is missing, dangling, or unreadable.
|
|
159
|
+
*/
|
|
160
|
+
export function collectSkillMdRecord(skillDir, fileType, source, skillFilename = 'SKILL.md', extraRaw = {}) {
|
|
161
|
+
let info;
|
|
162
|
+
try {
|
|
163
|
+
info = inspectSkillLink(skillDir, skillFilename);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
info = unknownInspectFailure(skillDir, skillFilename, err);
|
|
167
|
+
}
|
|
168
|
+
if (!shouldEmitSkillSlot(info, skillFilename))
|
|
169
|
+
return null;
|
|
170
|
+
const mdPath = join(skillDir, skillFilename);
|
|
171
|
+
const raw = {
|
|
172
|
+
source,
|
|
173
|
+
...extraRaw,
|
|
174
|
+
};
|
|
175
|
+
const symlink_info = skillLinkPayload(info);
|
|
176
|
+
if (info.status === 'valid') {
|
|
177
|
+
const content = readMarkdownFile(mdPath);
|
|
178
|
+
if (content !== null) {
|
|
179
|
+
raw.content = content;
|
|
180
|
+
}
|
|
181
|
+
else if (symlink_info.symlink_enabled) {
|
|
182
|
+
const skill_md = symlink_info.skill_md;
|
|
183
|
+
const skill_folder = symlink_info.skill_folder;
|
|
184
|
+
if (skill_md)
|
|
185
|
+
skill_md.status = 'inaccessible';
|
|
186
|
+
else if (skill_folder)
|
|
187
|
+
skill_folder.status = 'inaccessible';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { file_type: fileType, file_path: mdPath, raw_content: raw, symlink_info };
|
|
191
|
+
}
|
|
@@ -75,6 +75,20 @@ export function runSkillsListJson(args, cwd) {
|
|
|
75
75
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
function assignListLockFields(row, out) {
|
|
79
|
+
for (const key of ['source', 'sourceUrl', 'sourceType']) {
|
|
80
|
+
if (!Object.prototype.hasOwnProperty.call(row, key))
|
|
81
|
+
continue;
|
|
82
|
+
const raw = row[key];
|
|
83
|
+
if (raw == null) {
|
|
84
|
+
out[key] = null;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const trimmed = String(raw).trim();
|
|
88
|
+
out[key] = trimmed || null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
78
92
|
function normalizeListRows(rows, scope) {
|
|
79
93
|
const out = [];
|
|
80
94
|
for (const row of rows) {
|
|
@@ -85,12 +99,14 @@ function normalizeListRows(rows, scope) {
|
|
|
85
99
|
const agents = Array.isArray(row.agents)
|
|
86
100
|
? row.agents.map((a) => String(a).trim()).filter(Boolean)
|
|
87
101
|
: [];
|
|
88
|
-
|
|
102
|
+
const entry = {
|
|
89
103
|
name,
|
|
90
104
|
path,
|
|
91
105
|
scope: (row.scope || scope).trim() || scope,
|
|
92
106
|
agents,
|
|
93
|
-
}
|
|
107
|
+
};
|
|
108
|
+
assignListLockFields(row, entry);
|
|
109
|
+
out.push(entry);
|
|
94
110
|
}
|
|
95
111
|
return out;
|
|
96
112
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** Org-pushed endpoint config. Prompt collection and later org options share this file. */
|
|
5
|
+
const ORG_CONFIG_RELATIVE_PATH = path.join(".optimuslabs", "management", "org", "config.json");
|
|
6
|
+
export function orgConfigPath() {
|
|
7
|
+
return path.join(os.homedir(), ORG_CONFIG_RELATIVE_PATH);
|
|
8
|
+
}
|
|
9
|
+
function readOrgConfig() {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(fs.readFileSync(orgConfigPath(), "utf8"));
|
|
12
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Missing or invalid — treat as empty.
|
|
18
|
+
}
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
function collectPromptsFromConfig(cfg) {
|
|
22
|
+
if (typeof cfg.collect_prompts === "boolean")
|
|
23
|
+
return cfg.collect_prompts;
|
|
24
|
+
// Previous on-disk key was inverted (true = hash / don't collect).
|
|
25
|
+
if (typeof cfg.hash_tool_call_prompts === "boolean")
|
|
26
|
+
return cfg.hash_tool_call_prompts !== true;
|
|
27
|
+
// No org config yet: match server default (collection off).
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
export function readCollectPrompts() {
|
|
31
|
+
return collectPromptsFromConfig(readOrgConfig());
|
|
32
|
+
}
|
|
33
|
+
export function persistCollectPrompts(value) {
|
|
34
|
+
if (typeof value !== "boolean")
|
|
35
|
+
return;
|
|
36
|
+
const filePath = orgConfigPath();
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
39
|
+
const next = { ...readOrgConfig(), collect_prompts: value };
|
|
40
|
+
delete next.hash_tool_call_prompts;
|
|
41
|
+
fs.writeFileSync(filePath, `${JSON.stringify(next)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Best-effort: next heartbeat can retry.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function persistCollectPromptsFromResponse(body) {
|
|
48
|
+
if (!body || typeof body !== "object")
|
|
49
|
+
return;
|
|
50
|
+
if (typeof body.collect_prompts === "boolean") {
|
|
51
|
+
persistCollectPrompts(body.collect_prompts);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (typeof body.hash_tool_call_prompts === "boolean") {
|
|
55
|
+
persistCollectPrompts(body.hash_tool_call_prompts !== true);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -4,6 +4,7 @@ import { loadEndpointBase } from './endpoint_config.js';
|
|
|
4
4
|
import { hookRunLog } from '../runtime/hook_logger.js';
|
|
5
5
|
import { canonicalCursorUserStateVscdbPath } from '../runtime/remediation_config_path.js';
|
|
6
6
|
import { resolveWorkspaceRepoFromEnv } from '../runtime/workspace_repo.js';
|
|
7
|
+
import { persistCollectPromptsFromResponse } from '../runtime/prompt-collection-policy.js';
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import os from 'node:os';
|
|
9
10
|
import path from 'node:path';
|
|
@@ -79,11 +80,16 @@ function buildBatchChunks(configFiles, basePayloadSize) {
|
|
|
79
80
|
return chunks;
|
|
80
81
|
}
|
|
81
82
|
function buildChunkBody(chunk, hardwareUuid, authKey, hookRequestId, metadata) {
|
|
82
|
-
const config_files = chunk.map((c) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
const config_files = chunk.map((c) => {
|
|
84
|
+
const item = {
|
|
85
|
+
file_type: c.file_type,
|
|
86
|
+
file_path: canonicalCursorUserStateVscdbPath(c.file_path),
|
|
87
|
+
raw_content: c.raw_content,
|
|
88
|
+
};
|
|
89
|
+
if (c.symlink_info !== undefined)
|
|
90
|
+
item.symlink_info = c.symlink_info;
|
|
91
|
+
return item;
|
|
92
|
+
});
|
|
87
93
|
const payload = { hardware_uuid: hardwareUuid, metadata, config_files };
|
|
88
94
|
if (hookRequestId != null)
|
|
89
95
|
payload.hook_request_id = hookRequestId;
|
|
@@ -127,6 +133,7 @@ async function sendConfigFilesBatch(configFiles, hardwareUuid, authKey, hookRequ
|
|
|
127
133
|
const timeoutMs = Math.min(20000 + Math.ceil(bodySize / (1024 * 1024)) * 15000, 90000);
|
|
128
134
|
try {
|
|
129
135
|
const response = (await postStartupPayload(apiUrl, body, timeoutMs));
|
|
136
|
+
persistCollectPromptsFromResponse(response);
|
|
130
137
|
if (response.status === 'accepted') {
|
|
131
138
|
totals.accepted += typeof response.accepted === 'number' ? response.accepted : chunk.length;
|
|
132
139
|
const failedList = Array.isArray(response.failed) ? response.failed : [];
|
|
@@ -221,6 +228,8 @@ async function sendConfigFile(configFile, hardwareUuid, authKey, repoIdentifier)
|
|
|
221
228
|
const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/log-config-file/`;
|
|
222
229
|
const uploadPath = canonicalCursorUserStateVscdbPath(configFile.file_path);
|
|
223
230
|
const payload = { hardware_uuid: hardwareUuid, file_type: configFile.file_type, file_path: uploadPath, raw_content: configFile.raw_content };
|
|
231
|
+
if (configFile.symlink_info !== undefined)
|
|
232
|
+
payload.symlink_info = configFile.symlink_info;
|
|
224
233
|
const signature = createSignature(payload, authKey.key);
|
|
225
234
|
const body = { ...payload, signature, key_id: authKey.key_id || '', metadata: { org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '', organization_uuid: readOrganizationUuid(), repo_identifier: repoIdentifier ?? resolveWorkspaceRepoFromEnv() } };
|
|
226
235
|
try {
|
|
@@ -5,6 +5,7 @@ import { resolveHardwareUuid } from './hardware_uuid.js';
|
|
|
5
5
|
import { writeAuthKey, readStoredAuthKey, loadEndpointBase, buildStartupEndpointUrl } from './auth_key_store.js';
|
|
6
6
|
import { resolveUserProfile } from './user_profile.js';
|
|
7
7
|
import { resolveWorkspaceRepo } from '../log_config_files/runtime/workspace_repo.js';
|
|
8
|
+
import { persistCollectPromptsFromResponse } from '../log_config_files/runtime/prompt-collection-policy.js';
|
|
8
9
|
import fs from 'node:fs';
|
|
9
10
|
import os from 'node:os';
|
|
10
11
|
import path from 'node:path';
|
|
@@ -83,6 +84,7 @@ const maybeSendToEndpoint = async (hardwareUuid, timestamp, options = {}) => {
|
|
|
83
84
|
const requestBody = buildRequestBody(hardwareUuid, timestamp);
|
|
84
85
|
try {
|
|
85
86
|
const response = await postStartupPayload(startupEndpointUrl, requestBody);
|
|
87
|
+
persistCollectPromptsFromResponse(response);
|
|
86
88
|
const result = classifyEndpointResponse(response);
|
|
87
89
|
console.log(result.message);
|
|
88
90
|
if (result.branch === 'key_issued' && result.key) {
|