ccprofiles-core 0.5.1 → 0.7.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 +15 -0
- package/dist/adopt.d.ts +1 -1
- package/dist/adopt.js +24 -3
- package/dist/apply.d.ts +4 -1
- package/dist/apply.js +43 -13
- package/dist/assets.d.ts +10 -2
- package/dist/assets.js +15 -5
- package/dist/codex.d.ts +4 -1
- package/dist/codex.js +10 -3
- package/dist/devices.js +1 -1
- package/dist/discovery.d.ts +4 -0
- package/dist/discovery.js +19 -0
- package/dist/fsutil.d.ts +3 -1
- package/dist/fsutil.js +14 -6
- package/dist/handoff.d.ts +21 -0
- package/dist/handoff.js +38 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/manifest.d.ts +35 -0
- package/dist/manifest.js +74 -5
- package/dist/platform.js +8 -1
- package/dist/plugins.d.ts +21 -0
- package/dist/plugins.js +56 -0
- package/dist/provider.d.ts +10 -0
- package/dist/provider.js +30 -0
- package/dist/rcblock.js +2 -0
- package/dist/secrets.js +5 -5
- package/dist/sessions.d.ts +35 -0
- package/dist/sessions.js +291 -67
- package/package.json +12 -2
package/dist/manifest.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import YAML from 'yaml';
|
|
3
|
-
import { mkdir, readFile
|
|
3
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
|
+
import { atomicWrite } from './fsutil.js';
|
|
7
8
|
const exec = promisify(execFile);
|
|
8
9
|
export class ManifestError extends Error {
|
|
9
10
|
}
|
|
@@ -26,19 +27,39 @@ const ProfileSchema = z.object({
|
|
|
26
27
|
settingsEnv: z.record(z.string()).default({}),
|
|
27
28
|
skipPermissions: z.boolean().default(false),
|
|
28
29
|
sharedSessions: z.boolean().default(false),
|
|
30
|
+
plugins: z.array(z.string()).default([]),
|
|
29
31
|
});
|
|
32
|
+
const MarketplaceSchema = z.object({ source: z.string().min(1) });
|
|
30
33
|
const ManifestSchema = z.object({
|
|
31
34
|
version: z.literal(1),
|
|
32
35
|
hub: z.string().nullable(),
|
|
33
36
|
profiles: z.array(ProfileSchema),
|
|
34
37
|
mcpServers: z.record(McpServerSchema),
|
|
38
|
+
marketplaces: z.record(MarketplaceSchema).default({}),
|
|
35
39
|
});
|
|
36
40
|
// identifiers that get interpolated into shell launcher code must be injection-safe
|
|
37
|
-
|
|
41
|
+
// leading '-' is forbidden (but internal '-' still allowed) so a value can never be parsed as a
|
|
42
|
+
// CLI flag when passed as a positional argv entry (e.g. `claude plugin install <id>`)
|
|
43
|
+
const SAFE_NAME = /^[A-Za-z0-9_][A-Za-z0-9_-]*$/; // profile names, launcher names, secret refs
|
|
38
44
|
const SAFE_ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/; // POSIX/PowerShell env var names
|
|
39
45
|
const SAFE_LINK_ENTRY = /^[A-Za-z0-9._-]+$/; // one profile-dir child; never a path
|
|
46
|
+
const SAFE_SOURCE = /^[A-Za-z0-9._@:/][A-Za-z0-9._/@:-]*$/; // marketplace source (interpolated into `claude plugin` shell-out); no leading '-'
|
|
40
47
|
const SHELL_META = /["`$;|&()\n\r<>]/; // chars that could break out of a quoted shell context
|
|
41
48
|
const SECRET_PREFIX = 'secret://';
|
|
49
|
+
// plaintext provider tokens that must never be committed to manifest git history
|
|
50
|
+
const PLAINTEXT_TOKEN = /sk-ant-[A-Za-z0-9_-]{8,}|sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}/;
|
|
51
|
+
/**
|
|
52
|
+
* True if `yaml` (raw manifest.yaml text, or any string) contains a plaintext provider
|
|
53
|
+
* token. saveManifest() uses this to decide whether to skip the git commit; doctor checks
|
|
54
|
+
* (CLI + UI) use it to surface that skip as a visible problem instead of a stderr-only warning.
|
|
55
|
+
*/
|
|
56
|
+
export function manifestHasPlaintextSecret(yaml) {
|
|
57
|
+
return PLAINTEXT_TOKEN.test(yaml);
|
|
58
|
+
}
|
|
59
|
+
/** Reject any path with a literal `..` path-traversal segment, on either `/` or `\` separators. */
|
|
60
|
+
function hasDotDotSegment(path) {
|
|
61
|
+
return path.split(/[/\\]/).includes('..');
|
|
62
|
+
}
|
|
42
63
|
/**
|
|
43
64
|
* Reject manifests whose identifiers could break out of the shell launcher context.
|
|
44
65
|
* Applies to every manifest — an untrusted bundle/peer manifest reaches this before
|
|
@@ -52,6 +73,8 @@ export function assertSafeManifest(m) {
|
|
|
52
73
|
throw new ManifestError(`unsafe launcher name in profile "${p.name}": ${JSON.stringify(p.launcher)}`);
|
|
53
74
|
if (SHELL_META.test(p.dir))
|
|
54
75
|
throw new ManifestError(`unsafe profile dir in profile "${p.name}": ${JSON.stringify(p.dir)}`);
|
|
76
|
+
if (hasDotDotSegment(p.dir))
|
|
77
|
+
throw new ManifestError(`unsafe profile dir in profile "${p.name}": ${JSON.stringify(p.dir)} (must not contain a ".." segment)`);
|
|
55
78
|
for (const [k, v] of Object.entries(p.env)) {
|
|
56
79
|
if (!SAFE_ENV_KEY.test(k))
|
|
57
80
|
throw new ManifestError(`unsafe env var name in profile "${p.name}": ${JSON.stringify(k)}`);
|
|
@@ -75,6 +98,19 @@ export function assertSafeManifest(m) {
|
|
|
75
98
|
throw new ManifestError(`unsafe link entry in profile "${p.name}": ${JSON.stringify(entry)} (must be one file or directory name)`);
|
|
76
99
|
}
|
|
77
100
|
}
|
|
101
|
+
for (const [name, mk] of Object.entries(m.marketplaces ?? {})) {
|
|
102
|
+
if (!SAFE_NAME.test(name))
|
|
103
|
+
throw new ManifestError(`unsafe marketplace name: ${JSON.stringify(name)}`);
|
|
104
|
+
if (!SAFE_SOURCE.test(mk.source))
|
|
105
|
+
throw new ManifestError(`unsafe marketplace source for "${name}": ${JSON.stringify(mk.source)}`);
|
|
106
|
+
}
|
|
107
|
+
for (const p of m.profiles)
|
|
108
|
+
for (const id of p.plugins) {
|
|
109
|
+
const at = id.lastIndexOf('@');
|
|
110
|
+
const nm = at > 0 ? id.slice(0, at) : id, mkt = at > 0 ? id.slice(at + 1) : '';
|
|
111
|
+
if (!SAFE_NAME.test(nm) || !SAFE_NAME.test(mkt))
|
|
112
|
+
throw new ManifestError(`unsafe plugin id in profile "${p.name}": ${JSON.stringify(id)}`);
|
|
113
|
+
}
|
|
78
114
|
}
|
|
79
115
|
export function parseManifest(text) {
|
|
80
116
|
let raw;
|
|
@@ -92,6 +128,13 @@ export function parseManifest(text) {
|
|
|
92
128
|
for (const name of p.mcp)
|
|
93
129
|
if (!m.mcpServers[name])
|
|
94
130
|
throw new ManifestError(`profile "${p.name}" references undefined mcp server "${name}"`);
|
|
131
|
+
for (const p of m.profiles)
|
|
132
|
+
for (const id of p.plugins) {
|
|
133
|
+
const at = id.lastIndexOf('@');
|
|
134
|
+
const mkt = at > 0 ? id.slice(at + 1) : '';
|
|
135
|
+
if (!mkt || !m.marketplaces[mkt])
|
|
136
|
+
throw new ManifestError(`profile "${p.name}" references undefined marketplace for plugin "${id}"`);
|
|
137
|
+
}
|
|
95
138
|
assertSafeManifest(m);
|
|
96
139
|
return m;
|
|
97
140
|
}
|
|
@@ -104,12 +147,38 @@ export async function loadManifest(root) {
|
|
|
104
147
|
export async function saveManifest(root, m) {
|
|
105
148
|
const yaml = serializeManifest(m);
|
|
106
149
|
parseManifest(yaml); // guard: never write a manifest that cannot be reloaded
|
|
107
|
-
|
|
108
|
-
|
|
150
|
+
// manifestRoot also holds secrets.enc/devices.json — lock it down here since this is often
|
|
151
|
+
// the first thing (adopt/manifest init) to create it.
|
|
152
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
153
|
+
// settingsEnv can carry plaintext secrets until `secrets migrate` runs — write at 0600.
|
|
154
|
+
await atomicWrite(join(root, 'manifest.yaml'), yaml, { mode: 0o600 });
|
|
155
|
+
if (manifestHasPlaintextSecret(yaml)) {
|
|
156
|
+
// never let a plaintext token land in git history — the file is still written above
|
|
157
|
+
// (callers may still need it on disk), we just refuse to commit it.
|
|
158
|
+
process.stderr.write('warn: manifest contains a plaintext secret — skipping git commit; run: ccprofiles secrets migrate\n');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
109
161
|
try {
|
|
110
162
|
await exec('git', ['-C', root, 'rev-parse', '--git-dir']);
|
|
111
163
|
await exec('git', ['-C', root, 'add', '-A']);
|
|
112
164
|
await exec('git', ['-C', root, 'commit', '-m', 'ccprofiles: update manifest']);
|
|
113
165
|
}
|
|
114
|
-
catch {
|
|
166
|
+
catch (e) {
|
|
167
|
+
if (!isBenignGitError(e)) {
|
|
168
|
+
// manifest.yaml is already written above — don't throw, but don't silently drop history
|
|
169
|
+
// either. Anything other than "not a repo" / "nothing to commit" is a real failure
|
|
170
|
+
// (hook rejection, disk full, corrupt index, ...) the user should know about.
|
|
171
|
+
process.stderr.write(`warn: manifest.yaml written but git commit failed: ${gitErrorMessage(e)}\n`);
|
|
172
|
+
}
|
|
173
|
+
// else: not a git repo, or nothing changed to commit — expected, nothing to report
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** True for the two git outcomes saveManifest treats as expected/benign, not a real failure. */
|
|
177
|
+
function isBenignGitError(e) {
|
|
178
|
+
const text = gitErrorMessage(e);
|
|
179
|
+
return /nothing to commit/i.test(text) || /not a git repository/i.test(text);
|
|
180
|
+
}
|
|
181
|
+
function gitErrorMessage(e) {
|
|
182
|
+
const err = e;
|
|
183
|
+
return [err?.stderr, err?.stdout, err?.message].filter(Boolean).join(' ').trim() || String(e);
|
|
115
184
|
}
|
package/dist/platform.js
CHANGED
|
@@ -16,5 +16,12 @@ export function renderPath(template, p) {
|
|
|
16
16
|
export function toTemplate(absPath, p) {
|
|
17
17
|
const norm = absPath.replaceAll('\\', '/');
|
|
18
18
|
const home = p.home.replaceAll('\\', '/');
|
|
19
|
-
|
|
19
|
+
// require a path-separator boundary — a raw startsWith would also match sibling dirs
|
|
20
|
+
// that merely share a string prefix with home (e.g. home "/Users/tm" matching
|
|
21
|
+
// "/Users/tmfadhlul-old/...")
|
|
22
|
+
if (norm === home)
|
|
23
|
+
return '{home}';
|
|
24
|
+
if (norm.startsWith(home + '/'))
|
|
25
|
+
return '{home}' + norm.slice(home.length);
|
|
26
|
+
return norm;
|
|
20
27
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface PluginRunner {
|
|
2
|
+
marketplaceAdd(configDir: string, source: string): Promise<void>;
|
|
3
|
+
install(configDir: string, id: string): Promise<void>;
|
|
4
|
+
uninstall(configDir: string, id: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare function marketplaceOf(id: string): string | null;
|
|
7
|
+
export declare function planPluginReconcile(desired: string[], current: string[]): {
|
|
8
|
+
install: string[];
|
|
9
|
+
uninstall: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function reconcileProfilePlugins(opts: {
|
|
12
|
+
configDir: string;
|
|
13
|
+
desired: string[];
|
|
14
|
+
current: string[];
|
|
15
|
+
marketplaces: Record<string, {
|
|
16
|
+
source: string;
|
|
17
|
+
}>;
|
|
18
|
+
runner: PluginRunner;
|
|
19
|
+
}): Promise<string[]>;
|
|
20
|
+
/** If a profile's plugins/ is a legacy symlink into the old shared pool, restore it to a real dir. */
|
|
21
|
+
export declare function restoreLegacyPluginSymlink(pluginsDir: string): Promise<boolean>;
|
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { cp, lstat, mkdir, readlink, unlink } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
export function marketplaceOf(id) {
|
|
4
|
+
const at = id.lastIndexOf('@');
|
|
5
|
+
return at > 0 ? id.slice(at + 1) : null;
|
|
6
|
+
}
|
|
7
|
+
export function planPluginReconcile(desired, current) {
|
|
8
|
+
const d = new Set(desired), c = new Set(current);
|
|
9
|
+
return { install: desired.filter(id => !c.has(id)), uninstall: current.filter(id => !d.has(id)) };
|
|
10
|
+
}
|
|
11
|
+
export async function reconcileProfilePlugins(opts) {
|
|
12
|
+
const log = [];
|
|
13
|
+
const { install, uninstall } = planPluginReconcile(opts.desired, opts.current);
|
|
14
|
+
for (const id of uninstall) {
|
|
15
|
+
await opts.runner.uninstall(opts.configDir, id);
|
|
16
|
+
log.push(`uninstall ${id}`);
|
|
17
|
+
}
|
|
18
|
+
const added = new Set();
|
|
19
|
+
for (const id of install) {
|
|
20
|
+
const mkt = marketplaceOf(id);
|
|
21
|
+
if (mkt && !added.has(mkt)) {
|
|
22
|
+
const src = opts.marketplaces[mkt]?.source;
|
|
23
|
+
if (src) {
|
|
24
|
+
await opts.runner.marketplaceAdd(opts.configDir, src);
|
|
25
|
+
added.add(mkt);
|
|
26
|
+
log.push(`add ${src}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
await opts.runner.install(opts.configDir, id);
|
|
30
|
+
log.push(`install ${id}`);
|
|
31
|
+
}
|
|
32
|
+
return log;
|
|
33
|
+
}
|
|
34
|
+
/** If a profile's plugins/ is a legacy symlink into the old shared pool, restore it to a real dir. */
|
|
35
|
+
export async function restoreLegacyPluginSymlink(pluginsDir) {
|
|
36
|
+
let st = null;
|
|
37
|
+
try {
|
|
38
|
+
st = await lstat(pluginsDir);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (!st.isSymbolicLink())
|
|
44
|
+
return false;
|
|
45
|
+
const target = await readlink(pluginsDir);
|
|
46
|
+
// Never unlink-and-empty when the pool target is gone — that would silently leave the
|
|
47
|
+
// profile with a permanently empty plugins/ and no way to recover the original content.
|
|
48
|
+
// Leave the symlink in place and fail loudly instead.
|
|
49
|
+
if (!existsSync(target)) {
|
|
50
|
+
throw new Error(`cannot restore ${pluginsDir}: symlink target ${target} does not exist — leaving the symlink in place`);
|
|
51
|
+
}
|
|
52
|
+
await unlink(pluginsDir);
|
|
53
|
+
await mkdir(pluginsDir, { recursive: true });
|
|
54
|
+
await cp(target, pluginsDir, { recursive: true, force: false, errorOnExist: false });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type AnthropicAuthMode = 'login' | 'api-key' | 'auth-token';
|
|
2
|
+
/** Derive the current Anthropic auth mode from a settingsEnv map. */
|
|
3
|
+
export declare function anthropicAuthMode(env: Record<string, string>): AnthropicAuthMode;
|
|
4
|
+
/**
|
|
5
|
+
* New settingsEnv with `mode` applied. Non-auth keys preserved. `login` removes
|
|
6
|
+
* both token vars; `api-key`/`auth-token` set that var to `tokenRef` and remove
|
|
7
|
+
* the other. Throws if env has a custom ANTHROPIC_BASE_URL (non-Anthropic provider)
|
|
8
|
+
* or if a token mode is chosen without a tokenRef.
|
|
9
|
+
*/
|
|
10
|
+
export declare function setAnthropicAuthMode(env: Record<string, string>, mode: AnthropicAuthMode, tokenRef?: string): Record<string, string>;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const API_KEY = 'ANTHROPIC_API_KEY';
|
|
2
|
+
const AUTH_TOKEN = 'ANTHROPIC_AUTH_TOKEN';
|
|
3
|
+
const BASE_URL = 'ANTHROPIC_BASE_URL';
|
|
4
|
+
/** Derive the current Anthropic auth mode from a settingsEnv map. */
|
|
5
|
+
export function anthropicAuthMode(env) {
|
|
6
|
+
if (env[API_KEY] !== undefined)
|
|
7
|
+
return 'api-key';
|
|
8
|
+
if (env[AUTH_TOKEN] !== undefined)
|
|
9
|
+
return 'auth-token';
|
|
10
|
+
return 'login';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* New settingsEnv with `mode` applied. Non-auth keys preserved. `login` removes
|
|
14
|
+
* both token vars; `api-key`/`auth-token` set that var to `tokenRef` and remove
|
|
15
|
+
* the other. Throws if env has a custom ANTHROPIC_BASE_URL (non-Anthropic provider)
|
|
16
|
+
* or if a token mode is chosen without a tokenRef.
|
|
17
|
+
*/
|
|
18
|
+
export function setAnthropicAuthMode(env, mode, tokenRef) {
|
|
19
|
+
if (env[BASE_URL] !== undefined && env[BASE_URL].trim())
|
|
20
|
+
throw new Error(`profile uses a custom provider base URL (${env[BASE_URL]}) — manage its token in the Provider editor, not as an Anthropic auth mode`);
|
|
21
|
+
const out = { ...env };
|
|
22
|
+
delete out[API_KEY];
|
|
23
|
+
delete out[AUTH_TOKEN];
|
|
24
|
+
if (mode === 'login')
|
|
25
|
+
return out;
|
|
26
|
+
if (!tokenRef || !tokenRef.trim())
|
|
27
|
+
throw new Error(`${mode} mode requires a token reference`);
|
|
28
|
+
out[mode === 'api-key' ? API_KEY : AUTH_TOKEN] = tokenRef.trim();
|
|
29
|
+
return out;
|
|
30
|
+
}
|
package/dist/rcblock.js
CHANGED
|
@@ -18,6 +18,7 @@ function profileDirExpr(pr, p) {
|
|
|
18
18
|
}
|
|
19
19
|
function renderPosix(pr, p) {
|
|
20
20
|
const lines = [`${pr.launcher}() {`];
|
|
21
|
+
lines.push(` if [ "$1" = handoff ]; then shift; command ccprofiles handoff --from ${pr.name} --to "$1"; return; fi`);
|
|
21
22
|
for (const [k, v] of Object.entries(pr.env)) {
|
|
22
23
|
lines.push(v.startsWith(SECRET_PREFIX)
|
|
23
24
|
? ` export ${k}="$(ccprofiles secrets get ${v.slice(SECRET_PREFIX.length)})"`
|
|
@@ -32,6 +33,7 @@ function renderPosix(pr, p) {
|
|
|
32
33
|
}
|
|
33
34
|
function renderPwsh(pr, p) {
|
|
34
35
|
const lines = [`function ${pr.launcher} {`];
|
|
36
|
+
lines.push(` if ($args[0] -eq 'handoff') { ccprofiles handoff --from ${pr.name} --to $args[1]; return }`);
|
|
35
37
|
for (const [k, v] of Object.entries(pr.env)) {
|
|
36
38
|
lines.push(v.startsWith(SECRET_PREFIX)
|
|
37
39
|
? ` $env:${k} = (ccprofiles secrets get ${v.slice(SECRET_PREFIX.length)})`
|
package/dist/secrets.js
CHANGED
|
@@ -35,12 +35,12 @@ export class FileBackend {
|
|
|
35
35
|
const c = createCipheriv('aes-256-gcm', this.key(f.salt), iv);
|
|
36
36
|
const data = Buffer.concat([c.update(value, 'utf8'), c.final()]);
|
|
37
37
|
f.entries[key] = { iv: iv.toString('base64'), tag: c.getAuthTag().toString('base64'), data: data.toString('base64') };
|
|
38
|
-
await atomicWrite(this.filePath, JSON.stringify(f));
|
|
38
|
+
await atomicWrite(this.filePath, JSON.stringify(f), { mode: 0o600 });
|
|
39
39
|
}
|
|
40
40
|
async delete(key) {
|
|
41
41
|
const f = await this.load();
|
|
42
42
|
delete f.entries[key];
|
|
43
|
-
await atomicWrite(this.filePath, JSON.stringify(f));
|
|
43
|
+
await atomicWrite(this.filePath, JSON.stringify(f), { mode: 0o600 });
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
const realExec = promisify(execFile);
|
|
@@ -150,12 +150,12 @@ export class DpapiBackend {
|
|
|
150
150
|
async set(key, value) {
|
|
151
151
|
const f = await this.load();
|
|
152
152
|
f.entries[key] = await this.crypt.protect(value);
|
|
153
|
-
await atomicWrite(this.filePath, JSON.stringify(f));
|
|
153
|
+
await atomicWrite(this.filePath, JSON.stringify(f), { mode: 0o600 });
|
|
154
154
|
}
|
|
155
155
|
async delete(key) {
|
|
156
156
|
const f = await this.load();
|
|
157
157
|
delete f.entries[key];
|
|
158
|
-
await atomicWrite(this.filePath, JSON.stringify(f));
|
|
158
|
+
await atomicWrite(this.filePath, JSON.stringify(f), { mode: 0o600 });
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
export class SecretsStore {
|
|
@@ -171,7 +171,7 @@ export class SecretsStore {
|
|
|
171
171
|
return JSON.parse(await readFile(this.indexPath, 'utf8'));
|
|
172
172
|
}
|
|
173
173
|
async writeIndex(names) {
|
|
174
|
-
await atomicWrite(this.indexPath, JSON.stringify([...new Set(names)].sort()));
|
|
174
|
+
await atomicWrite(this.indexPath, JSON.stringify([...new Set(names)].sort()), { mode: 0o600 });
|
|
175
175
|
}
|
|
176
176
|
get backendName() { return this.backend.name; }
|
|
177
177
|
async get(key) { return this.backend.get(key); }
|
package/dist/sessions.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export interface SessionMeta {
|
|
|
6
6
|
gitBranch: string | null;
|
|
7
7
|
model: string | null;
|
|
8
8
|
sizeBytes: number;
|
|
9
|
+
/** true when the transcript was too large to fully scan (per-file cap) or the per-request
|
|
10
|
+
* byte budget ran out before reaching it — metadata is best-effort / incomplete. */
|
|
11
|
+
truncated?: boolean;
|
|
9
12
|
}
|
|
10
13
|
export interface ProjectSessions {
|
|
11
14
|
agent: 'claude' | 'codex';
|
|
@@ -13,6 +16,26 @@ export interface ProjectSessions {
|
|
|
13
16
|
project: string;
|
|
14
17
|
sessions: SessionMeta[];
|
|
15
18
|
}
|
|
19
|
+
export interface TranscriptEntry {
|
|
20
|
+
id: string;
|
|
21
|
+
role: 'user' | 'assistant' | 'tool';
|
|
22
|
+
text: string;
|
|
23
|
+
label: string | null;
|
|
24
|
+
timestamp: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface SessionTranscript {
|
|
27
|
+
id: string;
|
|
28
|
+
agent: 'claude' | 'codex';
|
|
29
|
+
scope: string;
|
|
30
|
+
project: string;
|
|
31
|
+
messages: TranscriptEntry[];
|
|
32
|
+
}
|
|
33
|
+
/** Test/observability hook: cumulative cache hit/miss counts across all scanSessions() calls
|
|
34
|
+
* in this process. Not reset automatically — callers that need deltas should diff two reads. */
|
|
35
|
+
export declare function sessionScanCacheStats(): {
|
|
36
|
+
hits: number;
|
|
37
|
+
misses: number;
|
|
38
|
+
};
|
|
16
39
|
/** Best-effort decode of Claude Code's project dir name (cwd from a record is preferred). */
|
|
17
40
|
export declare function decodeProjectDir(name: string): string;
|
|
18
41
|
export declare function scanSessions(opts: {
|
|
@@ -23,3 +46,15 @@ export declare function scanSessions(opts: {
|
|
|
23
46
|
agent?: 'claude' | 'codex';
|
|
24
47
|
}[];
|
|
25
48
|
}): Promise<ProjectSessions[]>;
|
|
49
|
+
/** Read visible conversation and tool activity for one known managed session. */
|
|
50
|
+
export declare function readSessionTranscript(opts: {
|
|
51
|
+
sharedRoot: string;
|
|
52
|
+
profiles: {
|
|
53
|
+
name: string;
|
|
54
|
+
dir: string;
|
|
55
|
+
agent?: 'claude' | 'codex';
|
|
56
|
+
}[];
|
|
57
|
+
agent: 'claude' | 'codex';
|
|
58
|
+
scope: string;
|
|
59
|
+
id: string;
|
|
60
|
+
}): Promise<SessionTranscript | null>;
|