@optimuslabs/harness-map-staging 1.5.39 → 1.5.41
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/agent_access/launchd_job.js +105 -0
- package/dist/agent_access/window_runner.js +74 -21
- package/dist/compliance_prompt_gate.js +2 -1
- package/dist/log_config_files/runtime/management_storage.js +15 -4
- package/dist/log_config_files/runtime/skill_block_sessions.js +9 -4
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the blocked-agent window process as a one-off launchd job in the user's GUI session
|
|
3
|
+
* (docs/agent-exception-requests/DESIGN.md), instead of as a child of the hook.
|
|
4
|
+
*
|
|
5
|
+
* A child of the hook lives in the agent's process tree and context: an agent that stops what its
|
|
6
|
+
* hook started once the hook returns, or runs hooks where no window can open (seen with old Cursor
|
|
7
|
+
* versions), left the user with no pop-up at all. launchd starts the job itself, in the session the
|
|
8
|
+
* Dock and Finder run in, so neither can reach it. The job runs once (no KeepAlive), from a plist in
|
|
9
|
+
* a folder of its own (mode 600); the window process removes the job when it is done, and jobs a
|
|
10
|
+
* crashed process left behind are removed the next time one starts. Nothing here needs root.
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { basename, dirname, join } from 'node:path';
|
|
17
|
+
const LABEL_PREFIX = 'io.optimuslabs.agent-window.';
|
|
18
|
+
/** A label this module made, and nothing else: the prefix and 12 hex characters. */
|
|
19
|
+
const OUR_LABEL = /^io\.optimuslabs\.agent-window\.[0-9a-f]{12}$/;
|
|
20
|
+
/** The user's GUI launchd domain, and nothing else. */
|
|
21
|
+
const GUI_DOMAIN = /^gui\/\d+$/;
|
|
22
|
+
/** The env names the window process needs (story id, endpoint, home, temp folder): nothing else. */
|
|
23
|
+
const PASSED_ENV = /^(OPTIMUS_[A-Z0-9_]+|HOME|USER|PATH|TMPDIR|LANG)$/;
|
|
24
|
+
/** Set on the job, so the window process knows its label and plist folder. */
|
|
25
|
+
export const JOB_LABEL_ENV = 'OPTIMUS_WINDOW_JOB';
|
|
26
|
+
const JOB_DIR_ENV = 'OPTIMUS_WINDOW_JOB_DIR';
|
|
27
|
+
function xml(value) {
|
|
28
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
29
|
+
}
|
|
30
|
+
/** The job's plist: run `program` once, now, with `env`. */
|
|
31
|
+
export function jobPlist(label, program, env) {
|
|
32
|
+
const args = program.map((a) => `<string>${xml(a)}</string>`).join('');
|
|
33
|
+
const vars = Object.entries(env)
|
|
34
|
+
.map(([k, v]) => `<key>${xml(k)}</key><string>${xml(v)}</string>`)
|
|
35
|
+
.join('');
|
|
36
|
+
return ('<?xml version="1.0" encoding="UTF-8"?>\n' +
|
|
37
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n' +
|
|
38
|
+
`<plist version="1.0"><dict><key>Label</key><string>${xml(label)}</string>` +
|
|
39
|
+
`<key>ProgramArguments</key><array>${args}</array>` +
|
|
40
|
+
`<key>EnvironmentVariables</key><dict>${vars}</dict>` +
|
|
41
|
+
'<key>RunAtLoad</key><true/><key>KeepAlive</key><false/></dict></plist>\n');
|
|
42
|
+
}
|
|
43
|
+
/** The env the job gets: what the window process needs from this one, and its own job details. */
|
|
44
|
+
function jobEnv(label, dir) {
|
|
45
|
+
const env = {};
|
|
46
|
+
for (const [k, v] of Object.entries(process.env))
|
|
47
|
+
if (PASSED_ENV.test(k) && v !== undefined)
|
|
48
|
+
env[k] = v;
|
|
49
|
+
env.TMPDIR = tmpdir();
|
|
50
|
+
env[JOB_LABEL_ENV] = label;
|
|
51
|
+
env[JOB_DIR_ENV] = dir;
|
|
52
|
+
return env;
|
|
53
|
+
}
|
|
54
|
+
function guiDomain() {
|
|
55
|
+
return typeof process.getuid === 'function' ? `gui/${process.getuid()}` : null;
|
|
56
|
+
}
|
|
57
|
+
/** Remove one of our jobs from the domain; anything that is not exactly ours is left alone. */
|
|
58
|
+
function bootout(domain, label) {
|
|
59
|
+
if (!GUI_DOMAIN.test(domain) || !OUR_LABEL.test(label))
|
|
60
|
+
return;
|
|
61
|
+
const target = `${domain}/${label}`;
|
|
62
|
+
// A fixed command, no shell, and a target checked above to be our own job in the user's domain.
|
|
63
|
+
spawnSync('launchctl', ['bootout', target]); // nosafe: SAFE801
|
|
64
|
+
}
|
|
65
|
+
/** Remove the window jobs no process runs any more (one a crashed window process left behind). */
|
|
66
|
+
function sweepFinishedJobs(domain) {
|
|
67
|
+
const out = spawnSync('launchctl', ['list'], { encoding: 'utf8' });
|
|
68
|
+
for (const line of (out.stdout || '').split('\n')) {
|
|
69
|
+
const [pid, , label] = line.trim().split(/\s+/);
|
|
70
|
+
if (pid === '-' && label)
|
|
71
|
+
bootout(domain, label);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Start `program` as a one-off launchd job in the user's GUI session; the job's label, or null when
|
|
76
|
+
* launchd would not take it (the caller then starts the process itself).
|
|
77
|
+
*/
|
|
78
|
+
export function startLaunchdJob(program) {
|
|
79
|
+
const domain = guiDomain();
|
|
80
|
+
if (!domain)
|
|
81
|
+
return null;
|
|
82
|
+
const dir = mkdtempSync(join(tmpdir(), 'optimus-window-job-'));
|
|
83
|
+
const label = `${LABEL_PREFIX}${randomBytes(6).toString('hex')}`;
|
|
84
|
+
const plist = join(dir, `${label}.plist`);
|
|
85
|
+
writeFileSync(plist, jobPlist(label, program, jobEnv(label, dir)), { mode: 0o600 });
|
|
86
|
+
sweepFinishedJobs(domain);
|
|
87
|
+
// A fixed command, no shell; the plist is one this process just wrote in its own folder.
|
|
88
|
+
const out = spawnSync('launchctl', ['bootstrap', domain, plist], { encoding: 'utf8' }); // nosafe: SAFE801
|
|
89
|
+
if (out.status === 0)
|
|
90
|
+
return label;
|
|
91
|
+
rmSync(dir, { recursive: true, force: true });
|
|
92
|
+
throw new Error(`launchctl bootstrap failed (exit ${out.status}): ${(out.stderr || '').trim()}`);
|
|
93
|
+
}
|
|
94
|
+
/** In the window process: remove its own job and plist folder, if launchd started it. */
|
|
95
|
+
export function endLaunchdJob() {
|
|
96
|
+
const label = process.env[JOB_LABEL_ENV] || '';
|
|
97
|
+
const dir = process.env[JOB_DIR_ENV] || '';
|
|
98
|
+
const domain = guiDomain();
|
|
99
|
+
if (dir && basename(dir).startsWith('optimus-window-job-') && dirname(dir) === tmpdir()) {
|
|
100
|
+
rmSync(dir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
// Last: removing the job ends this process.
|
|
103
|
+
if (domain)
|
|
104
|
+
bootout(domain, label);
|
|
105
|
+
}
|
|
@@ -2,24 +2,30 @@
|
|
|
2
2
|
* Shows the blocked-agent window (docs/agent-exception-requests/DESIGN.md).
|
|
3
3
|
*
|
|
4
4
|
* Started detached by the prompt gate (`launchAgentAccessWindow`), so the hook returns its block at
|
|
5
|
-
* once. Run as `node window_runner.js <agent> [<words.json>]`: an agent's own block reads the window's
|
|
5
|
+
* once. Run as `node window_runner.js <agent> <kind> [<words.json>]`: an agent's own block reads the window's
|
|
6
6
|
* words from the synced approvals; a tenant or email-domain block passes them, built by the gate, in
|
|
7
7
|
* a file of its own temporary folder. It draws the window with `osascript` (macOS only),
|
|
8
8
|
* then acts on what the user chose: Open launches the approved app, Cancel only closes it. The prompt
|
|
9
|
-
* was already blocked.
|
|
9
|
+
* was already blocked. Whether the window was shown, or why not (the `osascript` error), is reported
|
|
10
|
+
* into the prompt's story as a `compliance/agent_access_window` functional run, so a machine where `osascript`
|
|
11
|
+
* cannot draw it is visible on the server; the gate had already told the hook the window was up, so
|
|
12
|
+
* the user saw no pop-up at all. Nothing is ever thrown.
|
|
10
13
|
*/
|
|
11
14
|
import { spawn, spawnSync } from 'node:child_process';
|
|
12
15
|
import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
|
-
import { tmpdir } from 'node:os';
|
|
16
|
+
import { release, tmpdir } from 'node:os';
|
|
14
17
|
import { basename, dirname, join } from 'node:path';
|
|
15
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { reportFunctionalRun } from '../log_config_files/runtime/client_event_reporter.js';
|
|
16
20
|
import { hookRunLog } from '../log_config_files/runtime/hook_logger.js';
|
|
17
21
|
import { readRemediationInstructionsFile } from '../log_config_files/runtime/management_storage.js';
|
|
18
22
|
import { approvalId } from './decision.js';
|
|
23
|
+
import { endLaunchdJob, JOB_LABEL_ENV, startLaunchdJob } from './launchd_job.js';
|
|
19
24
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
25
|
const WINDOW_SCRIPT = join(HERE, '..', 'agent_block_window', 'agent_block_window.jxa');
|
|
21
26
|
/** The agents the gate checks, by approval id: nothing else is ever passed to a process. */
|
|
22
27
|
const AGENTS = ['claude', 'cursor', 'codex', 'copilot', 'opencode'];
|
|
28
|
+
const KINDS = ['agent', 'login', 'skill'];
|
|
23
29
|
/** A macOS bundle identifier, and nothing that could be read as an option or a path. */
|
|
24
30
|
const BUNDLE_ID = /^[A-Za-z0-9][A-Za-z0-9.-]{0,254}$/;
|
|
25
31
|
/** Where the gate leaves a login block's words for the runner: a folder of its own under tmpdir. */
|
|
@@ -56,18 +62,23 @@ function syncedWindow(agent) {
|
|
|
56
62
|
return readRemediationInstructionsFile().agent_access_windows?.[approvalId(agent)];
|
|
57
63
|
}
|
|
58
64
|
/**
|
|
59
|
-
* Start the runner in the background,
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
65
|
+
* Start the runner in the background, on macOS only; true when it started. `words` are a tenant or
|
|
66
|
+
* email-domain block's, built by the gate; without them the runner reads the agent's own block
|
|
67
|
+
* window from the synced approvals. False when there is nothing to show (an approvals file synced
|
|
68
|
+
* before the window existed), so Cursor's hook keeps its own alert.
|
|
69
|
+
*
|
|
70
|
+
* launchd starts it in the user's GUI session (`launchd_job.ts`), out of reach of what the agent
|
|
71
|
+
* does to its hook's processes; when launchd will not take the job, it is started here, detached.
|
|
63
72
|
*/
|
|
64
|
-
export function launchAgentAccessWindow(agent, words) {
|
|
65
|
-
if (process.platform !== 'darwin' || !AGENTS.includes(agent))
|
|
73
|
+
export function launchAgentAccessWindow(agent, kind, words) {
|
|
74
|
+
if (process.platform !== 'darwin' || !AGENTS.includes(agent) || !KINDS.includes(kind))
|
|
66
75
|
return false;
|
|
67
76
|
if (!words && !syncedWindow(agent))
|
|
68
77
|
return false;
|
|
69
78
|
try {
|
|
70
|
-
const args = [fileURLToPath(import.meta.url), agent, ...(words ? [writeWords(words)] : [])];
|
|
79
|
+
const args = [fileURLToPath(import.meta.url), agent, kind, ...(words ? [writeWords(words)] : [])];
|
|
80
|
+
if (startByLaunchd([process.execPath, ...args]))
|
|
81
|
+
return true;
|
|
71
82
|
const options = { detached: true, stdio: 'ignore' };
|
|
72
83
|
// An argument list, no shell: an agent from the fixed list above, and the path of a file it wrote.
|
|
73
84
|
const child = spawn(process.execPath, args, options); // nosafe: SAFE801
|
|
@@ -79,24 +90,48 @@ export function launchAgentAccessWindow(agent, words) {
|
|
|
79
90
|
return false;
|
|
80
91
|
}
|
|
81
92
|
}
|
|
93
|
+
/** Start the runner as a launchd job; false (logged) when launchd would not take it. */
|
|
94
|
+
function startByLaunchd(program) {
|
|
95
|
+
try {
|
|
96
|
+
return startLaunchdJob(program) !== null;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
hookRunLog(`agent_access_window: launchd: ${err instanceof Error ? err.message : String(err)}`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The installed Cursor's version ('' for another agent or when it cannot be read). */
|
|
104
|
+
function agentVersion(agent) {
|
|
105
|
+
if (agent !== 'cursor')
|
|
106
|
+
return '';
|
|
107
|
+
const plist = '/Applications/Cursor.app/Contents/Info.plist';
|
|
108
|
+
const out = spawnSync('plutil', ['-extract', 'CFBundleShortVersionString', 'raw', plist], { encoding: 'utf8' });
|
|
109
|
+
return out.status === 0 ? (out.stdout || '').trim().slice(0, 32) : '';
|
|
110
|
+
}
|
|
82
111
|
/** Draw the window and return what the user did; null when it could not open. */
|
|
83
112
|
export function showWindow(words) {
|
|
113
|
+
return drawWindow(words).result;
|
|
114
|
+
}
|
|
115
|
+
/** Draw the window: what the user did, or why it could not be shown (`error`, '' when it was). */
|
|
116
|
+
export function drawWindow(words) {
|
|
84
117
|
if (!words)
|
|
85
|
-
return null;
|
|
118
|
+
return { result: null, error: 'no words to show' };
|
|
86
119
|
const dir = mkdtempSync(join(tmpdir(), 'optimus-window-'));
|
|
87
120
|
const payloadPath = join(dir, 'payload.json');
|
|
88
121
|
try {
|
|
89
122
|
writeFileSync(payloadPath, JSON.stringify({ words }));
|
|
90
123
|
const out = spawnSync('osascript', ['-l', 'JavaScript', WINDOW_SCRIPT, payloadPath], { encoding: 'utf8' });
|
|
91
124
|
if (out.status !== 0) {
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
const error = `osascript failed (exit ${out.status ?? out.signal}): ${(out.stderr || out.error?.message || '').trim()}`;
|
|
126
|
+
hookRunLog(`agent_access_window: ${error}`);
|
|
127
|
+
return { result: null, error };
|
|
94
128
|
}
|
|
95
|
-
return JSON.parse(out.stdout.trim());
|
|
129
|
+
return { result: JSON.parse(out.stdout.trim()), error: '' };
|
|
96
130
|
}
|
|
97
131
|
catch (err) {
|
|
98
|
-
|
|
99
|
-
|
|
132
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
133
|
+
hookRunLog(`agent_access_window: ${error}`);
|
|
134
|
+
return { result: null, error };
|
|
100
135
|
}
|
|
101
136
|
finally {
|
|
102
137
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -115,15 +150,33 @@ export function actOn(result) {
|
|
|
115
150
|
if (result.action === 'open')
|
|
116
151
|
openApp(result.bundle_id ?? '');
|
|
117
152
|
}
|
|
118
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Show the window, act on the choice, and report into the prompt's story whether it was shown
|
|
155
|
+
* (the runner inherits the gate's story id). Awaited, so the report is sent before the runner exits.
|
|
156
|
+
*/
|
|
157
|
+
export async function runAgentAccessWindow(agent, kind, wordsPath = '') {
|
|
158
|
+
const launched = process.env[JOB_LABEL_ENV] ? 'launchd' : 'spawn';
|
|
159
|
+
const metadata = { darwin: release(), kind, launched, agent_version: agentVersion(agent) };
|
|
160
|
+
// The server knows a fixed set of units; the window is part of the prompt gate's compliance.
|
|
161
|
+
// "started" first: a window process the agent stopped never reports more, and that shows.
|
|
162
|
+
const report = { unit: 'compliance', command: 'agent_access_window', metadata };
|
|
163
|
+
await reportFunctionalRun({ ...report, outcome: 'started', message: `${agent}: window process started (${launched})` });
|
|
119
164
|
const words = wordsPath ? readWords(wordsPath) : syncedWindow(agent);
|
|
120
|
-
const result =
|
|
165
|
+
const { result, error } = drawWindow(words ?? undefined);
|
|
121
166
|
if (result)
|
|
122
167
|
actOn(result);
|
|
168
|
+
await reportFunctionalRun({
|
|
169
|
+
...report,
|
|
170
|
+
outcome: result ? 'ok' : 'failed',
|
|
171
|
+
message: (result ? `${agent}: shown, closed with ${result.action}` : `${agent}: not shown: ${error}`).slice(0, 500),
|
|
172
|
+
detail: error,
|
|
173
|
+
});
|
|
174
|
+
endLaunchdJob();
|
|
123
175
|
}
|
|
124
176
|
const isEntryPoint = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
125
177
|
if (isEntryPoint) {
|
|
126
|
-
const [agent, wordsPath] = process.argv.slice(2);
|
|
127
|
-
if (agent && AGENTS.includes(agent))
|
|
128
|
-
runAgentAccessWindow(agent, wordsPath);
|
|
178
|
+
const [agent, kind, wordsPath] = process.argv.slice(2);
|
|
179
|
+
if (agent && AGENTS.includes(agent) && KINDS.includes(kind)) {
|
|
180
|
+
void runAgentAccessWindow(agent, kind, wordsPath);
|
|
181
|
+
}
|
|
129
182
|
}
|
|
@@ -334,7 +334,8 @@ function blockedByLogin(agent, ide) {
|
|
|
334
334
|
// The blocked-agent window opens beside every block, in the background, so the hook answers at
|
|
335
335
|
// once: an agent's own from the synced approvals, a login's from the words built here. Where it
|
|
336
336
|
// could not start, Cursor's hook keeps its alert.
|
|
337
|
-
|
|
337
|
+
// A login block (tenant, email domain) brings its own words; the agent's own comes from the sync.
|
|
338
|
+
const windowShown = launchAgentAccessWindow(approvalId(agent), block.window ? 'login' : 'agent', block.window);
|
|
338
339
|
console.log(agent === 'codex' ? codexBlockPayload(block.reason) : blockPayload(ide, block.reason, windowShown));
|
|
339
340
|
return true;
|
|
340
341
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Paths and JSON persistence for ~/.optimuslabs/management:
|
|
3
3
|
* - remediation_instructions.json (remediations[])
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
6
7
|
import { dirname, join } from 'node:path';
|
|
7
8
|
import { homedir } from 'node:os';
|
|
8
9
|
import { OPTIMUSLABS_MANAGEMENT_REL, OPTIMUSLABS_LOGS_REL } from '../../bootstrap_constants.js';
|
|
@@ -123,13 +124,23 @@ export function writeFileCollectionVscdbContract(contract) {
|
|
|
123
124
|
return;
|
|
124
125
|
atomicWriteJson(getFileCollectionVscdbContractPath(), sanitized);
|
|
125
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Write `data` as JSON to `filePath` in one step: to a temporary file of this write's own, then a
|
|
129
|
+
* rename. Two hooks writing at once (two Claude Code windows, the gate and the compliance runner)
|
|
130
|
+
* each use their own temporary file, so neither can rename the other's half-written one into place.
|
|
131
|
+
*/
|
|
126
132
|
export function atomicWriteJson(filePath, data) {
|
|
127
133
|
const dir = dirname(filePath);
|
|
128
134
|
if (!existsSync(dir))
|
|
129
135
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
130
|
-
const tmp = `${filePath}.tmp`;
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
const tmp = `${filePath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
137
|
+
try {
|
|
138
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
|
|
139
|
+
renameSync(tmp, filePath);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
rmSync(tmp, { force: true });
|
|
143
|
+
}
|
|
133
144
|
}
|
|
134
145
|
function parseInstructionsRaw(raw) {
|
|
135
146
|
try {
|
|
@@ -16,11 +16,16 @@ import { basename } from 'node:path';
|
|
|
16
16
|
import { atomicWriteJson, getSkillBlockProcessesPath, readRemediationInstructionsFile, } from './management_storage.js';
|
|
17
17
|
import { launchAgentAccessWindow } from '../../agent_access/window_runner.js';
|
|
18
18
|
import { quarantinedAt } from './skill_block.js';
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* A process's parent, start time and command name, from `ps`; null when it cannot be read. `ps`
|
|
21
|
+
* writes the start time in the user's language ("Do. 5 Nov." in German), which the pattern below
|
|
22
|
+
* cannot read, so it runs in the C locale.
|
|
23
|
+
*/
|
|
24
|
+
export function processRow(pid) {
|
|
21
25
|
try {
|
|
26
|
+
const options = { encoding: 'utf8', env: { ...process.env, LC_ALL: 'C' } };
|
|
22
27
|
// A fixed command, no shell, and a process id that is always a number.
|
|
23
|
-
const out = execFileSync('ps', ['-o', 'ppid=,lstart=,comm=', '-p', String(pid)],
|
|
28
|
+
const out = execFileSync('ps', ['-o', 'ppid=,lstart=,comm=', '-p', String(pid)], options).trim(); // nosafe: SAFE801
|
|
24
29
|
const row = out.match(/^(\d+)\s+(\S+\s+\S+\s+\d+\s+[\d:]+\s+\d+)\s+(.*)$/);
|
|
25
30
|
return row ? { parent: Number(row[1]), startedAt: Date.parse(row[2]), name: basename(row[3]).toLowerCase() } : null;
|
|
26
31
|
}
|
|
@@ -170,7 +175,7 @@ function openWindowOf(uuid) {
|
|
|
170
175
|
if (!words)
|
|
171
176
|
return false;
|
|
172
177
|
windowsShown.add(uuid);
|
|
173
|
-
return launchAgentAccessWindow('claude', words);
|
|
178
|
+
return launchAgentAccessWindow('claude', 'skill', words);
|
|
174
179
|
}
|
|
175
180
|
/** Open the window beside the block of the skill named `skill` (a stopped session, a typed command). */
|
|
176
181
|
export function openSkillBlockWindow(skill) {
|