@bridge4dev/runner 0.11.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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/adapters/claude.d.ts +19 -0
- package/dist/adapters/claude.js +631 -0
- package/dist/adapters/codex-home.d.ts +61 -0
- package/dist/adapters/codex-home.js +234 -0
- package/dist/adapters/codex-protocol.d.ts +59 -0
- package/dist/adapters/codex-protocol.js +204 -0
- package/dist/adapters/codex.d.ts +61 -0
- package/dist/adapters/codex.js +1406 -0
- package/dist/adapters/types.d.ts +183 -0
- package/dist/adapters/types.js +5 -0
- package/dist/async-queue.d.ts +11 -0
- package/dist/async-queue.js +50 -0
- package/dist/attachments.d.ts +72 -0
- package/dist/attachments.js +149 -0
- package/dist/auth-relay.d.ts +57 -0
- package/dist/auth-relay.js +289 -0
- package/dist/config.d.ts +96 -0
- package/dist/config.js +73 -0
- package/dist/fsview.d.ts +20 -0
- package/dist/fsview.js +122 -0
- package/dist/git.d.ts +54 -0
- package/dist/git.js +168 -0
- package/dist/gitops.d.ts +136 -0
- package/dist/gitops.js +596 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +352 -0
- package/dist/journal.d.ts +118 -0
- package/dist/journal.js +300 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +19 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +33 -0
- package/dist/policy.d.ts +17 -0
- package/dist/policy.js +272 -0
- package/dist/protocol.d.ts +754 -0
- package/dist/protocol.js +154 -0
- package/dist/self-update.d.ts +75 -0
- package/dist/self-update.js +221 -0
- package/dist/status-file.d.ts +14 -0
- package/dist/status-file.js +29 -0
- package/dist/supervisor.d.ts +216 -0
- package/dist/supervisor.js +1648 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/ws-client.d.ts +30 -0
- package/dist/ws-client.js +171 -0
- package/package.json +52 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { TrustMode } from '../policy.js';
|
|
2
|
+
export interface McpConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Agent-agnostic interaction mode (session-5 plan §2).
|
|
8
|
+
* ask — ask before acting (Claude `default`, Codex `on-request`)
|
|
9
|
+
* plan — plan first, act after approval (Claude `plan`, Codex on-request + plan)
|
|
10
|
+
* auto — apply edits silently (Claude `acceptEdits`, Codex `on-failure`)
|
|
11
|
+
* full — never ask (Claude `bypassPermissions`, Codex `never`)
|
|
12
|
+
*/
|
|
13
|
+
export type AgentMode = 'ask' | 'plan' | 'auto' | 'full';
|
|
14
|
+
export declare const AGENT_MODES: readonly AgentMode[];
|
|
15
|
+
export declare function isAgentMode(value: unknown): value is AgentMode;
|
|
16
|
+
/**
|
|
17
|
+
* A reasoning-effort level a model supports. Codex advertises these per model
|
|
18
|
+
* (`supportedReasoningEfforts`) and the set differs between models — the new
|
|
19
|
+
* Sol/Terra/Luna line adds `max`/`ultra` on top of low/medium/high/xhigh — so
|
|
20
|
+
* the list is never hard-coded, it always comes from the agent.
|
|
21
|
+
*/
|
|
22
|
+
export interface EffortOption {
|
|
23
|
+
/** Identifier passed back to setEffort/startSession (e.g. `high`). */
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ModelOption {
|
|
29
|
+
/** Identifier passed back to setModel/startSession. */
|
|
30
|
+
id: string;
|
|
31
|
+
label: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
isDefault?: boolean;
|
|
34
|
+
/** Empty/absent when the model has no reasoning-effort dial. */
|
|
35
|
+
efforts?: EffortOption[];
|
|
36
|
+
/** The level the agent uses when none is chosen. */
|
|
37
|
+
defaultEffort?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface CommandOption {
|
|
40
|
+
/** Without the leading slash. */
|
|
41
|
+
name: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
argumentHint?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface AgentAccountInfo {
|
|
46
|
+
email?: string;
|
|
47
|
+
organization?: string;
|
|
48
|
+
plan?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface McpServerStatusInfo {
|
|
51
|
+
name: string;
|
|
52
|
+
status: string;
|
|
53
|
+
}
|
|
54
|
+
/** Everything the dashboard needs to render agent controls, live from the agent. */
|
|
55
|
+
export interface AgentCapabilities {
|
|
56
|
+
models: ModelOption[];
|
|
57
|
+
modes: AgentMode[];
|
|
58
|
+
commands: CommandOption[];
|
|
59
|
+
currentModel?: string;
|
|
60
|
+
/** Reasoning effort in force, when the agent has that dial. */
|
|
61
|
+
currentEffort?: string;
|
|
62
|
+
currentMode: AgentMode;
|
|
63
|
+
account?: AgentAccountInfo;
|
|
64
|
+
mcpServers?: McpServerStatusInfo[];
|
|
65
|
+
/** Interrupting a running turn is supported. */
|
|
66
|
+
interrupt: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface SessionSpec {
|
|
69
|
+
sessionId: string;
|
|
70
|
+
/** Working directory for the agent — the session worktree. */
|
|
71
|
+
cwd: string;
|
|
72
|
+
/**
|
|
73
|
+
* Fully composed initial prompt (ticket context included by the supervisor).
|
|
74
|
+
* Empty/undefined for free CHAT sessions: the agent boots and waits for the
|
|
75
|
+
* first message instead of starting a turn.
|
|
76
|
+
*/
|
|
77
|
+
prompt?: string;
|
|
78
|
+
trustMode: TrustMode;
|
|
79
|
+
mode?: AgentMode;
|
|
80
|
+
model?: string;
|
|
81
|
+
effort?: string;
|
|
82
|
+
resumeProviderSessionId?: string;
|
|
83
|
+
mcp?: McpConfig;
|
|
84
|
+
maxBudgetUsd?: number;
|
|
85
|
+
}
|
|
86
|
+
export type AgentEvent = {
|
|
87
|
+
type: 'message';
|
|
88
|
+
role: 'assistant' | 'user';
|
|
89
|
+
text: string;
|
|
90
|
+
} | {
|
|
91
|
+
type: 'thinking';
|
|
92
|
+
text: string;
|
|
93
|
+
}
|
|
94
|
+
/** The agent needs a decision from the user before it can continue. */
|
|
95
|
+
| {
|
|
96
|
+
type: 'question';
|
|
97
|
+
text: string;
|
|
98
|
+
options?: string[];
|
|
99
|
+
} | {
|
|
100
|
+
type: 'tool';
|
|
101
|
+
phase: 'use' | 'result';
|
|
102
|
+
name: string;
|
|
103
|
+
toolUseId?: string;
|
|
104
|
+
detail?: Record<string, unknown>;
|
|
105
|
+
text?: string;
|
|
106
|
+
} | {
|
|
107
|
+
type: 'permission';
|
|
108
|
+
requestId: string;
|
|
109
|
+
toolName: string;
|
|
110
|
+
title: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
input: Record<string, unknown>;
|
|
113
|
+
/** Plan approval (Claude ExitPlanMode / Codex plan item) — rendered as a plan card. */
|
|
114
|
+
plan?: string;
|
|
115
|
+
} | {
|
|
116
|
+
type: 'permission_resolved';
|
|
117
|
+
requestId: string;
|
|
118
|
+
allow: boolean;
|
|
119
|
+
source: 'user' | 'policy' | 'abort';
|
|
120
|
+
reason?: string;
|
|
121
|
+
} | {
|
|
122
|
+
type: 'provider_session';
|
|
123
|
+
providerSessionId: string;
|
|
124
|
+
model?: string;
|
|
125
|
+
} | {
|
|
126
|
+
type: 'capabilities';
|
|
127
|
+
capabilities: AgentCapabilities;
|
|
128
|
+
} | {
|
|
129
|
+
type: 'settings';
|
|
130
|
+
model?: string;
|
|
131
|
+
mode?: AgentMode;
|
|
132
|
+
effort?: string | null;
|
|
133
|
+
} | {
|
|
134
|
+
type: 'context_usage';
|
|
135
|
+
usedTokens: number;
|
|
136
|
+
maxTokens: number;
|
|
137
|
+
} | {
|
|
138
|
+
type: 'notice';
|
|
139
|
+
level: 'info' | 'warn';
|
|
140
|
+
text: string;
|
|
141
|
+
} | {
|
|
142
|
+
type: 'cost';
|
|
143
|
+
costUsd: number;
|
|
144
|
+
numTurns?: number;
|
|
145
|
+
durationMs?: number;
|
|
146
|
+
} | {
|
|
147
|
+
type: 'turn_end';
|
|
148
|
+
ok: boolean;
|
|
149
|
+
errorMessage?: string;
|
|
150
|
+
} | {
|
|
151
|
+
type: 'error';
|
|
152
|
+
message: string;
|
|
153
|
+
/**
|
|
154
|
+
* `auth_missing` — no login on this server at all.
|
|
155
|
+
* `auth_expired` — a login exists but the provider rejected it.
|
|
156
|
+
* Kept apart because the remedies differ, and calling a server that was
|
|
157
|
+
* never signed in "expired" sent people looking for a problem that did
|
|
158
|
+
* not exist.
|
|
159
|
+
*/
|
|
160
|
+
code?: 'resume_failed' | 'auth_expired' | 'auth_missing';
|
|
161
|
+
};
|
|
162
|
+
export interface AgentSession {
|
|
163
|
+
/** Ends when the underlying agent process is gone. */
|
|
164
|
+
events: AsyncIterable<AgentEvent>;
|
|
165
|
+
answerPermission(requestId: string, allow: boolean, note?: string): void;
|
|
166
|
+
/** Follow-up user input into the live session. */
|
|
167
|
+
send(text: string): void;
|
|
168
|
+
/** Switch model mid-session (VS-Code-extension parity). */
|
|
169
|
+
setModel(model: string): Promise<void>;
|
|
170
|
+
/** Switch reasoning effort mid-session; null = the model's own default. */
|
|
171
|
+
setEffort(effort: string | null): Promise<void>;
|
|
172
|
+
/** Switch interaction mode mid-session. */
|
|
173
|
+
setMode(mode: AgentMode): Promise<void>;
|
|
174
|
+
/** Interrupt the current turn (session stays resumable). */
|
|
175
|
+
interrupt(): Promise<void>;
|
|
176
|
+
/** Tear the session down (kills the agent process). */
|
|
177
|
+
stop(): void;
|
|
178
|
+
}
|
|
179
|
+
export interface AgentAdapter {
|
|
180
|
+
readonly id: 'claude' | 'codex';
|
|
181
|
+
startSession(spec: SessionSpec): AgentSession;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class AsyncQueue<T> implements AsyncIterable<T> {
|
|
2
|
+
private readonly values;
|
|
3
|
+
private readonly waiters;
|
|
4
|
+
private ended;
|
|
5
|
+
/** Returns false when the queue has ended and the value was dropped. */
|
|
6
|
+
push(value: T): boolean;
|
|
7
|
+
end(): void;
|
|
8
|
+
get isEnded(): boolean;
|
|
9
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=async-queue.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Minimal push-based async queue — bridges callback-style producers (WS
|
|
2
|
+
// frames, SDK messages) into AsyncIterable consumers.
|
|
3
|
+
export class AsyncQueue {
|
|
4
|
+
values = [];
|
|
5
|
+
waiters = [];
|
|
6
|
+
ended = false;
|
|
7
|
+
/** Returns false when the queue has ended and the value was dropped. */
|
|
8
|
+
push(value) {
|
|
9
|
+
if (this.ended)
|
|
10
|
+
return false;
|
|
11
|
+
const waiter = this.waiters.shift();
|
|
12
|
+
if (waiter) {
|
|
13
|
+
waiter({ value, done: false });
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
this.values.push(value);
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
end() {
|
|
21
|
+
if (this.ended)
|
|
22
|
+
return;
|
|
23
|
+
this.ended = true;
|
|
24
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
25
|
+
waiter({ value: undefined, done: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
get isEnded() {
|
|
29
|
+
return this.ended;
|
|
30
|
+
}
|
|
31
|
+
[Symbol.asyncIterator]() {
|
|
32
|
+
return {
|
|
33
|
+
next: () => {
|
|
34
|
+
const value = this.values.shift();
|
|
35
|
+
if (value !== undefined) {
|
|
36
|
+
return Promise.resolve({ value, done: false });
|
|
37
|
+
}
|
|
38
|
+
if (this.ended) {
|
|
39
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
40
|
+
}
|
|
41
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
42
|
+
},
|
|
43
|
+
return: () => {
|
|
44
|
+
this.end();
|
|
45
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=async-queue.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files the user attached to a message, brought onto their own machine
|
|
3
|
+
* (session 10).
|
|
4
|
+
*
|
|
5
|
+
* Why they land INSIDE the session worktree: layer-1 policy lets the agent read
|
|
6
|
+
* only within its worktree (policy.ts `isInsideWorktree`), and Claude Code and
|
|
7
|
+
* Codex both sandbox to their working directory. A file in `<state>/…` would
|
|
8
|
+
* cost a permission card on every single read — or be refused outright.
|
|
9
|
+
*
|
|
10
|
+
* Why that does not pollute the git panel: `.devbridge/` is added once to the
|
|
11
|
+
* repository's `info/exclude`, so `git status` and the diff never see it.
|
|
12
|
+
*/
|
|
13
|
+
/** Directory inside the worktree; also the line written to info/exclude. */
|
|
14
|
+
export declare const ATTACHMENT_DIR = ".devbridge/attachments";
|
|
15
|
+
export interface RemoteAttachment {
|
|
16
|
+
id: string;
|
|
17
|
+
fileName: string;
|
|
18
|
+
mimeType: string;
|
|
19
|
+
fileSize: number;
|
|
20
|
+
}
|
|
21
|
+
export interface SavedAttachment {
|
|
22
|
+
/** Path relative to the worktree — what the agent is told to open. */
|
|
23
|
+
relativePath: string;
|
|
24
|
+
fileName: string;
|
|
25
|
+
mimeType: string;
|
|
26
|
+
fileSize: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A file name that is safe to write and unambiguous to read.
|
|
30
|
+
*
|
|
31
|
+
* The name comes from the user's own machine and travels through our API, so it
|
|
32
|
+
* is treated as hostile: no directory separators, no leading dots, no control
|
|
33
|
+
* characters, bounded length. The attachment id prefix keeps two files called
|
|
34
|
+
* `screenshot.png` apart without inventing a counter.
|
|
35
|
+
*/
|
|
36
|
+
export declare function safeAttachmentName(id: string, fileName: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Keep `.devbridge/` out of git for this repository.
|
|
39
|
+
*
|
|
40
|
+
* Uses `info/exclude` rather than `.gitignore`: it is local-only, so we never
|
|
41
|
+
* add a line to a file the user commits. `--git-common-dir` matters because a
|
|
42
|
+
* linked worktree's `.git` is a file, and the shared exclude list is what both
|
|
43
|
+
* the worktree and the main checkout consult.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ensureGitExclude(worktreePath: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Download one message's attachments into the session worktree.
|
|
48
|
+
*
|
|
49
|
+
* Returns what actually landed — a file that could not be fetched is reported
|
|
50
|
+
* and skipped rather than failing the whole message, because the text the user
|
|
51
|
+
* typed is usually still worth delivering.
|
|
52
|
+
*/
|
|
53
|
+
export declare function saveAttachments(input: {
|
|
54
|
+
worktreePath: string;
|
|
55
|
+
apiUrl: string;
|
|
56
|
+
token: string;
|
|
57
|
+
attachments: RemoteAttachment[];
|
|
58
|
+
fetchImpl?: typeof fetch;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
saved: SavedAttachment[];
|
|
61
|
+
failed: string[];
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Turn the user's message plus the files into one prompt.
|
|
65
|
+
*
|
|
66
|
+
* Plain paths and an explicit instruction, deliberately: both agents read local
|
|
67
|
+
* files with their own tools, and an agent-specific encoding (image blocks for
|
|
68
|
+
* Claude, `localImage` items for Codex) would be two code paths that drift.
|
|
69
|
+
* The user's own words stay first — the files are context, not the request.
|
|
70
|
+
*/
|
|
71
|
+
export declare function composeMessageWithAttachments(text: string, saved: SavedAttachment[]): string;
|
|
72
|
+
//# sourceMappingURL=attachments.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { log } from './log.js';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
/**
|
|
8
|
+
* Files the user attached to a message, brought onto their own machine
|
|
9
|
+
* (session 10).
|
|
10
|
+
*
|
|
11
|
+
* Why they land INSIDE the session worktree: layer-1 policy lets the agent read
|
|
12
|
+
* only within its worktree (policy.ts `isInsideWorktree`), and Claude Code and
|
|
13
|
+
* Codex both sandbox to their working directory. A file in `<state>/…` would
|
|
14
|
+
* cost a permission card on every single read — or be refused outright.
|
|
15
|
+
*
|
|
16
|
+
* Why that does not pollute the git panel: `.devbridge/` is added once to the
|
|
17
|
+
* repository's `info/exclude`, so `git status` and the diff never see it.
|
|
18
|
+
*/
|
|
19
|
+
/** Directory inside the worktree; also the line written to info/exclude. */
|
|
20
|
+
export const ATTACHMENT_DIR = '.devbridge/attachments';
|
|
21
|
+
/** Hard ceiling per file; the API allows 5 MB for images and 25 MB for documents. */
|
|
22
|
+
const MAX_ATTACHMENT_BYTES = 26 * 1024 * 1024;
|
|
23
|
+
const DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
24
|
+
/**
|
|
25
|
+
* A file name that is safe to write and unambiguous to read.
|
|
26
|
+
*
|
|
27
|
+
* The name comes from the user's own machine and travels through our API, so it
|
|
28
|
+
* is treated as hostile: no directory separators, no leading dots, no control
|
|
29
|
+
* characters, bounded length. The attachment id prefix keeps two files called
|
|
30
|
+
* `screenshot.png` apart without inventing a counter.
|
|
31
|
+
*/
|
|
32
|
+
export function safeAttachmentName(id, fileName) {
|
|
33
|
+
const base = path
|
|
34
|
+
.basename(fileName)
|
|
35
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
36
|
+
.replace(/^\.+/, '');
|
|
37
|
+
const trimmed = base.slice(-80) || 'file';
|
|
38
|
+
return `${id.slice(0, 8)}-${trimmed}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Keep `.devbridge/` out of git for this repository.
|
|
42
|
+
*
|
|
43
|
+
* Uses `info/exclude` rather than `.gitignore`: it is local-only, so we never
|
|
44
|
+
* add a line to a file the user commits. `--git-common-dir` matters because a
|
|
45
|
+
* linked worktree's `.git` is a file, and the shared exclude list is what both
|
|
46
|
+
* the worktree and the main checkout consult.
|
|
47
|
+
*/
|
|
48
|
+
export async function ensureGitExclude(worktreePath) {
|
|
49
|
+
try {
|
|
50
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--git-common-dir'], {
|
|
51
|
+
cwd: worktreePath,
|
|
52
|
+
timeout: 10_000,
|
|
53
|
+
});
|
|
54
|
+
const commonDir = path.resolve(worktreePath, stdout.trim());
|
|
55
|
+
const infoDir = path.join(commonDir, 'info');
|
|
56
|
+
const excludeFile = path.join(infoDir, 'exclude');
|
|
57
|
+
const line = '/.devbridge/';
|
|
58
|
+
let current = '';
|
|
59
|
+
try {
|
|
60
|
+
current = fs.readFileSync(excludeFile, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* no exclude file yet */
|
|
64
|
+
}
|
|
65
|
+
if (current.split('\n').some((entry) => entry.trim() === line))
|
|
66
|
+
return;
|
|
67
|
+
fs.mkdirSync(infoDir, { recursive: true });
|
|
68
|
+
const prefix = current.length === 0 || current.endsWith('\n') ? '' : '\n';
|
|
69
|
+
fs.appendFileSync(excludeFile, `${prefix}# DevBridge: files you attach to a session live here\n${line}\n`);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
// Not fatal: the worst case is that attachments show up as untracked files.
|
|
73
|
+
log.warn('attachments: could not update info/exclude', { error: String(error) });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Download one message's attachments into the session worktree.
|
|
78
|
+
*
|
|
79
|
+
* Returns what actually landed — a file that could not be fetched is reported
|
|
80
|
+
* and skipped rather than failing the whole message, because the text the user
|
|
81
|
+
* typed is usually still worth delivering.
|
|
82
|
+
*/
|
|
83
|
+
export async function saveAttachments(input) {
|
|
84
|
+
const doFetch = input.fetchImpl ?? fetch;
|
|
85
|
+
const dir = path.join(input.worktreePath, ATTACHMENT_DIR);
|
|
86
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
87
|
+
await ensureGitExclude(input.worktreePath);
|
|
88
|
+
const saved = [];
|
|
89
|
+
const failed = [];
|
|
90
|
+
const base = input.apiUrl.replace(/\/$/, '');
|
|
91
|
+
for (const attachment of input.attachments) {
|
|
92
|
+
try {
|
|
93
|
+
const response = await doFetch(`${base}/api/v1/dev/runner/attachments/${encodeURIComponent(attachment.id)}`, {
|
|
94
|
+
headers: { Authorization: `Bearer ${input.token}` },
|
|
95
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`HTTP ${response.status}`);
|
|
99
|
+
}
|
|
100
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
101
|
+
if (buffer.length > MAX_ATTACHMENT_BYTES) {
|
|
102
|
+
throw new Error(`file is larger than ${Math.round(MAX_ATTACHMENT_BYTES / 1024 / 1024)}MB`);
|
|
103
|
+
}
|
|
104
|
+
const name = safeAttachmentName(attachment.id, attachment.fileName);
|
|
105
|
+
fs.writeFileSync(path.join(dir, name), buffer, { mode: 0o600 });
|
|
106
|
+
saved.push({
|
|
107
|
+
relativePath: `${ATTACHMENT_DIR}/${name}`,
|
|
108
|
+
fileName: attachment.fileName,
|
|
109
|
+
mimeType: attachment.mimeType,
|
|
110
|
+
fileSize: buffer.length,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
log.warn('attachments: download failed', {
|
|
115
|
+
attachmentId: attachment.id,
|
|
116
|
+
error: String(error),
|
|
117
|
+
});
|
|
118
|
+
failed.push(attachment.fileName);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { saved, failed };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Turn the user's message plus the files into one prompt.
|
|
125
|
+
*
|
|
126
|
+
* Plain paths and an explicit instruction, deliberately: both agents read local
|
|
127
|
+
* files with their own tools, and an agent-specific encoding (image blocks for
|
|
128
|
+
* Claude, `localImage` items for Codex) would be two code paths that drift.
|
|
129
|
+
* The user's own words stay first — the files are context, not the request.
|
|
130
|
+
*/
|
|
131
|
+
export function composeMessageWithAttachments(text, saved) {
|
|
132
|
+
if (saved.length === 0)
|
|
133
|
+
return text;
|
|
134
|
+
const lines = saved.map((file) => `- ${file.relativePath} — ${file.fileName} (${file.mimeType}, ${sizeLabel(file.fileSize)})`);
|
|
135
|
+
const header = saved.length === 1
|
|
136
|
+
? 'The user attached a file. It is already saved in this workspace:'
|
|
137
|
+
: 'The user attached files. They are already saved in this workspace:';
|
|
138
|
+
return [text.trim(), '', header, ...lines, '', 'Open them before answering — images included.']
|
|
139
|
+
.join('\n')
|
|
140
|
+
.trim();
|
|
141
|
+
}
|
|
142
|
+
function sizeLabel(bytes) {
|
|
143
|
+
if (bytes < 1024)
|
|
144
|
+
return `${bytes} B`;
|
|
145
|
+
if (bytes < 1024 * 1024)
|
|
146
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
147
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=attachments.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type RelayAgent = 'claude' | 'codex';
|
|
2
|
+
export interface LoginStartResult {
|
|
3
|
+
url: string;
|
|
4
|
+
/** Device-auth style user code to enter on the provider page (Codex). */
|
|
5
|
+
code?: string;
|
|
6
|
+
/** true → the flow finishes by pasting a confirmation code back (Claude). */
|
|
7
|
+
expectsCode: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface LoginCodeResult {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
detail?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AgentAuthStatus {
|
|
14
|
+
/**
|
|
15
|
+
* `missing` — never signed in here (or the CLI is not installed).
|
|
16
|
+
* `expired` — we hold a credential and it is past its expiry.
|
|
17
|
+
* `unknown` — the probe itself failed; say so instead of guessing.
|
|
18
|
+
*
|
|
19
|
+
* `expired` used to be the catch-all, which is how a server that had simply
|
|
20
|
+
* never signed in — and one whose probe merely timed out — both ended up
|
|
21
|
+
* telling the user their login had expired.
|
|
22
|
+
*/
|
|
23
|
+
status: 'ok' | 'expired' | 'missing' | 'unknown';
|
|
24
|
+
expiresAt?: string;
|
|
25
|
+
detail?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Strip ANSI/OSC control sequences so text matching sees plain output. */
|
|
28
|
+
export declare function stripControl(raw: string): string;
|
|
29
|
+
export declare function extractLoginUrl(agent: RelayAgent, raw: string): string | null;
|
|
30
|
+
export declare function extractDeviceCode(raw: string): string | null;
|
|
31
|
+
export declare class AuthRelay {
|
|
32
|
+
private readonly commands;
|
|
33
|
+
private active;
|
|
34
|
+
constructor(commands?: Record<RelayAgent, string>);
|
|
35
|
+
/** Start (or restart) a login flow and wait until the sign-in URL appears. */
|
|
36
|
+
start(agent: RelayAgent): Promise<LoginStartResult>;
|
|
37
|
+
/** Paste the confirmation code back into the waiting CLI (Claude flow). */
|
|
38
|
+
submitCode(agent: RelayAgent, code: string): Promise<LoginCodeResult>;
|
|
39
|
+
cancel(): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Claude: the subscription token's expiry is recorded in the CLI's own
|
|
43
|
+
* credentials file. The RUNNER reads it (its own host user's file) — the
|
|
44
|
+
* agent itself is still denied this path by layer-1 policy.
|
|
45
|
+
*/
|
|
46
|
+
export declare function claudeAuthStatus(homedir?: string): Promise<AgentAuthStatus>;
|
|
47
|
+
/**
|
|
48
|
+
* Codex reports its own login state via an exit code (0 signed in / 1 not).
|
|
49
|
+
* Probed against the RUNNER's home: the host user can be signed in while our
|
|
50
|
+
* isolated home is not, and it is ours that sessions use.
|
|
51
|
+
*/
|
|
52
|
+
export declare function codexAuthStatus(): Promise<AgentAuthStatus>;
|
|
53
|
+
export declare function agentAuthStatuses(): Promise<{
|
|
54
|
+
claude: AgentAuthStatus;
|
|
55
|
+
codex: AgentAuthStatus;
|
|
56
|
+
}>;
|
|
57
|
+
//# sourceMappingURL=auth-relay.d.ts.map
|