@phuetz/code-buddy 1.0.0 → 1.2.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 +160 -164
- package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
- package/dist/agent/codebuddy-agent.d.ts +1 -1
- package/dist/agent/codebuddy-agent.js +101 -13
- package/dist/agent/execution/agent-executor.js +122 -12
- package/dist/agent/execution/tool-dependency-graph.js +7 -0
- package/dist/agent/execution/tool-hooks.d.ts +6 -0
- package/dist/agent/execution/tool-hooks.js +13 -5
- package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
- package/dist/agent/execution/tool-selection-strategy.js +67 -10
- package/dist/agent/extended-thinking.d.ts +8 -0
- package/dist/agent/extended-thinking.js +27 -0
- package/dist/agent/hermes-browser-backends.js +151 -11
- package/dist/agent/hermes-claw-migrate.d.ts +59 -0
- package/dist/agent/hermes-claw-migrate.js +427 -26
- package/dist/agent/hermes-parity-manifest.js +78 -71
- package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
- package/dist/agent/hermes-runtime-lifecycle.js +468 -0
- package/dist/agent/lesson-auto-proposer.js +10 -0
- package/dist/agent/middleware/index.d.ts +1 -0
- package/dist/agent/middleware/index.js +1 -0
- package/dist/agent/middleware/session-duration.d.ts +36 -0
- package/dist/agent/middleware/session-duration.js +78 -0
- package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
- package/dist/agent/middleware/visual-validation-middleware.js +67 -0
- package/dist/agent/model-benchmark.d.ts +77 -0
- package/dist/agent/model-benchmark.js +309 -0
- package/dist/agent/model-tier.d.ts +14 -0
- package/dist/agent/model-tier.js +71 -0
- package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
- package/dist/agent/multi-agent/multi-agent-system.js +3 -1
- package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
- package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
- package/dist/agent/multi-agent/session-tools.js +22 -5
- package/dist/agent/reasoning/mcts.js +20 -6
- package/dist/agent/session-end-flush.d.ts +71 -0
- package/dist/agent/session-end-flush.js +233 -0
- package/dist/agent/streaming/streaming-handler.d.ts +7 -0
- package/dist/agent/streaming/streaming-handler.js +8 -1
- package/dist/agent/tool-executor.js +1 -0
- package/dist/agent/tool-handler.d.ts +5 -0
- package/dist/agent/tool-handler.js +130 -4
- package/dist/browser-automation/browser-operator-executor.js +15 -0
- package/dist/browser-automation/browser-use-runner.d.ts +96 -0
- package/dist/browser-automation/browser-use-runner.js +492 -0
- package/dist/browser-automation/camofox-runner.d.ts +107 -0
- package/dist/browser-automation/camofox-runner.js +287 -0
- package/dist/channels/dingtalk/index.js +3 -0
- package/dist/channels/discord/client.d.ts +9 -0
- package/dist/channels/discord/client.js +12 -0
- package/dist/channels/feishu/index.d.ts +167 -1
- package/dist/channels/feishu/index.js +432 -7
- package/dist/channels/gateway-lifecycle.d.ts +132 -0
- package/dist/channels/gateway-lifecycle.js +219 -0
- package/dist/channels/google-chat/index.d.ts +5 -1
- package/dist/channels/google-chat/index.js +5 -1
- package/dist/channels/imessage/index.d.ts +14 -0
- package/dist/channels/imessage/index.js +64 -17
- package/dist/channels/index.d.ts +4 -0
- package/dist/channels/index.js +4 -0
- package/dist/channels/irc/index.d.ts +73 -3
- package/dist/channels/irc/index.js +446 -11
- package/dist/channels/line/index.js +3 -0
- package/dist/channels/mattermost/index.d.ts +51 -4
- package/dist/channels/mattermost/index.js +303 -20
- package/dist/channels/nextcloud-talk/index.d.ts +89 -8
- package/dist/channels/nextcloud-talk/index.js +367 -16
- package/dist/channels/nostr/index.d.ts +121 -1
- package/dist/channels/nostr/index.js +396 -8
- package/dist/channels/ntfy/index.js +3 -0
- package/dist/channels/qq/index.js +3 -0
- package/dist/channels/slash-parity.d.ts +117 -0
- package/dist/channels/slash-parity.js +185 -0
- package/dist/channels/synology-chat/index.js +3 -0
- package/dist/channels/teams/index.d.ts +3 -0
- package/dist/channels/teams/index.js +3 -0
- package/dist/channels/telegram/client.d.ts +9 -0
- package/dist/channels/telegram/client.js +12 -0
- package/dist/channels/twilio-voice/index.js +3 -0
- package/dist/channels/webchat/index.d.ts +7 -1
- package/dist/channels/webchat/index.js +7 -1
- package/dist/channels/wecom/index.js +3 -0
- package/dist/channels/weixin/index.js +3 -0
- package/dist/channels/zalo/index.js +3 -0
- package/dist/cli/config-loader.js +1 -1
- package/dist/codebuddy/client.d.ts +30 -1
- package/dist/codebuddy/client.js +167 -20
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
- package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
- package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
- package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
- package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
- package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
- package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
- package/dist/codebuddy/tool-definitions/index.js +2 -1
- package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
- package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
- package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
- package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
- package/dist/codebuddy/tools.d.ts +1 -1
- package/dist/codebuddy/tools.js +27 -24
- package/dist/commands/cli/hermes-commands.js +108 -0
- package/dist/commands/cli/native-engine-commands.js +107 -3
- package/dist/commands/cli/utility-commands.js +45 -0
- package/dist/commands/enhanced-command-handler.js +5 -0
- package/dist/commands/goal-cli.d.ts +71 -0
- package/dist/commands/goal-cli.js +280 -0
- package/dist/commands/handlers/goal-handler.d.ts +34 -0
- package/dist/commands/handlers/goal-handler.js +155 -0
- package/dist/commands/handlers/index.d.ts +1 -0
- package/dist/commands/handlers/index.js +2 -0
- package/dist/commands/handlers/infra-handlers.js +36 -0
- package/dist/commands/handlers/memory-handlers.js +96 -1
- package/dist/commands/headless-slash.d.ts +2 -0
- package/dist/commands/headless-slash.js +12 -0
- package/dist/commands/llm-provider-resolution.d.ts +5 -3
- package/dist/commands/llm-provider-resolution.js +87 -33
- package/dist/commands/ollama.d.ts +25 -0
- package/dist/commands/ollama.js +100 -0
- package/dist/commands/provider.d.ts +5 -0
- package/dist/commands/provider.js +106 -55
- package/dist/commands/slash/builtin-commands.js +20 -0
- package/dist/commands/spec-next.js +2 -1
- package/dist/commands/spec-plan.js +5 -16
- package/dist/commands/tunnel.d.ts +2 -0
- package/dist/commands/tunnel.js +48 -0
- package/dist/config/config-resolver.d.ts +2 -1
- package/dist/config/config-resolver.js +54 -41
- package/dist/config/constants.d.ts +28 -0
- package/dist/config/constants.js +7 -0
- package/dist/config/env-schema.js +770 -0
- package/dist/config/feature-flags.js +7 -0
- package/dist/config/model-tools.js +4 -4
- package/dist/config/toml-config.d.ts +16 -0
- package/dist/config/toml-config.js +3 -0
- package/dist/context/context-manager-v2.d.ts +39 -0
- package/dist/context/context-manager-v2.js +91 -0
- package/dist/daemon/agent-task-executor.js +12 -1
- package/dist/daemon/autonomous-daemon.d.ts +1 -1
- package/dist/daemon/autonomous-daemon.js +5 -3
- package/dist/daemon/autonomous-loop.d.ts +21 -1
- package/dist/daemon/autonomous-loop.js +69 -0
- package/dist/daemon/colab-goal.d.ts +38 -0
- package/dist/daemon/colab-goal.js +81 -0
- package/dist/daemon/cron-agent-bridge.d.ts +12 -3
- package/dist/daemon/cron-agent-bridge.js +25 -9
- package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
- package/dist/desktop/codebuddy-engine-adapter.js +257 -80
- package/dist/desktop/engine-adapter.d.ts +14 -0
- package/dist/desktop-automation/automation-manager.js +16 -0
- package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
- package/dist/desktop-automation/omniparser-runner.js +115 -0
- package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
- package/dist/desktop-automation/smart-snapshot.js +72 -5
- package/dist/doctor/index.js +40 -23
- package/dist/events/types.d.ts +11 -0
- package/dist/fleet/colab-store.d.ts +21 -0
- package/dist/fleet/colab-store.js +28 -0
- package/dist/fleet/peer-session-bridge.d.ts +1 -1
- package/dist/fleet/peer-session-bridge.js +243 -2
- package/dist/fleet/peer-session-store.d.ts +3 -0
- package/dist/fleet/peer-tool-bridge.js +14 -0
- package/dist/fleet/privacy-lint.d.ts +8 -0
- package/dist/fleet/privacy-lint.js +22 -0
- package/dist/goals/goal-decomposer.d.ts +27 -0
- package/dist/goals/goal-decomposer.js +293 -0
- package/dist/goals/goal-judge-client.d.ts +15 -0
- package/dist/goals/goal-judge-client.js +45 -0
- package/dist/goals/goal-judge.d.ts +38 -0
- package/dist/goals/goal-judge.js +158 -0
- package/dist/goals/goal-loop.d.ts +37 -0
- package/dist/goals/goal-loop.js +101 -0
- package/dist/goals/goal-manager.d.ts +77 -0
- package/dist/goals/goal-manager.js +289 -0
- package/dist/goals/goal-state.d.ts +99 -0
- package/dist/goals/goal-state.js +315 -0
- package/dist/goals/goal-store.d.ts +28 -0
- package/dist/goals/goal-store.js +87 -0
- package/dist/goals/index.d.ts +5 -0
- package/dist/goals/index.js +6 -0
- package/dist/hooks/use-input-handler.js +36 -1
- package/dist/hooks/user-hooks.js +17 -3
- package/dist/index.js +506 -25
- package/dist/input/text-to-speech.d.ts +2 -6
- package/dist/input/text-to-speech.js +2 -27
- package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
- package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
- package/dist/integrations/tailscale.d.ts +13 -0
- package/dist/integrations/tailscale.js +87 -34
- package/dist/knowledge/workspace-indexer.js +53 -9
- package/dist/mcp/client.js +1 -0
- package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
- package/dist/mcp/mcp-desktop-tools.js +158 -0
- package/dist/mcp/mcp-server.js +2 -0
- package/dist/memory/enhanced-memory.d.ts +10 -0
- package/dist/memory/enhanced-memory.js +33 -2
- package/dist/memory/index.d.ts +3 -1
- package/dist/memory/index.js +4 -1
- package/dist/memory/memory-auto-proposer.d.ts +23 -0
- package/dist/memory/memory-auto-proposer.js +308 -0
- package/dist/memory/memory-candidate-queue.d.ts +92 -0
- package/dist/memory/memory-candidate-queue.js +261 -0
- package/dist/memory/persistent-memory.d.ts +45 -2
- package/dist/memory/persistent-memory.js +236 -40
- package/dist/ml/bayesian-qualifier.d.ts +1 -1
- package/dist/ml/bayesian-qualifier.js +35 -2
- package/dist/observability/run-store.d.ts +1 -1
- package/dist/openclaw/gateway-bridge.js +5 -0
- package/dist/plugins/marketplace.d.ts +1 -0
- package/dist/plugins/marketplace.js +7 -0
- package/dist/prompts/execution-discipline.d.ts +14 -0
- package/dist/prompts/execution-discipline.js +29 -0
- package/dist/prompts/variation-injector.js +9 -2
- package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
- package/dist/protocols/acp/acp-agentic-runner.js +115 -18
- package/dist/protocols/acp/acp-session-store.d.ts +23 -0
- package/dist/protocols/acp/acp-session-store.js +77 -0
- package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
- package/dist/protocols/acp/acp-stdio-server.js +71 -16
- package/dist/providers/active-llm-registry.d.ts +37 -0
- package/dist/providers/active-llm-registry.js +186 -0
- package/dist/providers/auxiliary-provider.d.ts +25 -0
- package/dist/providers/auxiliary-provider.js +192 -0
- package/dist/providers/codex-oauth.d.ts +1 -1
- package/dist/providers/codex-oauth.js +27 -4
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/provider-catalog.d.ts +70 -0
- package/dist/providers/provider-catalog.js +738 -0
- package/dist/providers/provider-fallback.d.ts +35 -0
- package/dist/providers/provider-fallback.js +171 -0
- package/dist/providers/xai-oauth.d.ts +128 -0
- package/dist/providers/xai-oauth.js +735 -0
- package/dist/scheduler/cron-scheduler.d.ts +15 -3
- package/dist/scheduler/cron-scheduler.js +53 -7
- package/dist/scheduler/script-runner.d.ts +2 -0
- package/dist/scheduler/script-runner.js +19 -4
- package/dist/scheduler/watchdog-handlers.js +7 -20
- package/dist/search/usearch-index.js +7 -2
- package/dist/security/tool-policy/tool-groups.js +2 -0
- package/dist/server/index.js +17 -2
- package/dist/server/routes/mobile.d.ts +12 -4
- package/dist/server/routes/mobile.js +116 -1
- package/dist/server/tls-config.d.ts +35 -0
- package/dist/server/tls-config.js +142 -0
- package/dist/server/tunnel-manager.d.ts +20 -0
- package/dist/server/tunnel-manager.js +58 -0
- package/dist/server/websocket/fleet-bridge.d.ts +13 -1
- package/dist/server/websocket/fleet-bridge.js +16 -0
- package/dist/services/prompt-builder.d.ts +1 -0
- package/dist/services/prompt-builder.js +49 -8
- package/dist/shared/engine-types.d.ts +15 -1
- package/dist/sidecar/sidecar-bridge.d.ts +1 -0
- package/dist/sidecar/sidecar-bridge.js +21 -10
- package/dist/spec/spec-store.js +8 -1
- package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
- package/dist/talk-mode/providers/audioreader-tts.js +8 -8
- package/dist/templates/project-scaffolding.js +3 -2
- package/dist/themes/theme-schema.d.ts +10 -10
- package/dist/tools/application-profiles.js +38 -0
- package/dist/tools/bash/bash-tool.d.ts +1 -0
- package/dist/tools/bash/bash-tool.js +16 -2
- package/dist/tools/bash/command-validator.js +3 -0
- package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
- package/dist/tools/bash/rtk-rewrite.js +100 -0
- package/dist/tools/bash/streaming-executor.js +14 -1
- package/dist/tools/computer-control-tool.d.ts +16 -1
- package/dist/tools/computer-control-tool.js +317 -5
- package/dist/tools/document-generator.d.ts +14 -0
- package/dist/tools/document-generator.js +79 -0
- package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
- package/dist/tools/execute-code-rpc-invoker.js +46 -5
- package/dist/tools/metadata.js +28 -0
- package/dist/tools/office-macro-tool.d.ts +10 -0
- package/dist/tools/office-macro-tool.js +93 -0
- package/dist/tools/registry/index.d.ts +2 -1
- package/dist/tools/registry/index.js +2 -1
- package/dist/tools/registry/memory-tools.d.ts +20 -0
- package/dist/tools/registry/memory-tools.js +225 -3
- package/dist/tools/registry/multimodal-tools.js +2 -2
- package/dist/tools/registry/vision-tools.d.ts +33 -0
- package/dist/tools/registry/vision-tools.js +308 -6
- package/dist/tools/registry/windows-tools.d.ts +2 -0
- package/dist/tools/registry/windows-tools.js +47 -0
- package/dist/tools/route-peer-tool.js +14 -0
- package/dist/tools/screenshot-tool.js +14 -2
- package/dist/tools/text-to-speech-tool.d.ts +1 -1
- package/dist/tools/text-to-speech-tool.js +2 -12
- package/dist/tools/tool-selector.js +3 -59
- package/dist/utils/config-validation/schema.d.ts +40 -7
- package/dist/utils/config-validation/schema.js +130 -1
- package/dist/utils/cost-tracker.js +21 -1
- package/dist/utils/disk-guard.d.ts +180 -0
- package/dist/utils/disk-guard.js +404 -0
- package/dist/utils/interactive-setup.js +35 -2
- package/dist/utils/model-utils.js +11 -2
- package/dist/utils/output-sanitizer.js +11 -0
- package/dist/utils/provider-detector.d.ts +8 -7
- package/dist/utils/provider-detector.js +19 -77
- package/dist/utils/settings-manager.d.ts +7 -0
- package/dist/utils/token-display.d.ts +3 -3
- package/dist/utils/token-display.js +22 -4
- package/dist/wizard/onboarding.d.ts +39 -0
- package/dist/wizard/onboarding.js +433 -21
- package/dist/wizard/provider-onboarding.d.ts +8 -23
- package/dist/wizard/provider-onboarding.js +39 -158
- package/package.json +4 -2
- package/dist/plugins/provider-onboarding.d.ts +0 -23
- package/dist/plugins/provider-onboarding.js +0 -116
- package/dist/utils/kokoro-tts.d.ts +0 -8
- package/dist/utils/kokoro-tts.js +0 -67
|
@@ -140,6 +140,10 @@ export interface JobRun {
|
|
|
140
140
|
result?: unknown;
|
|
141
141
|
error?: string;
|
|
142
142
|
duration?: number;
|
|
143
|
+
/** Structured output from this job run, passed as inputData to chained jobs. */
|
|
144
|
+
outputData?: string;
|
|
145
|
+
/** Input data received from a parent job in a chain. */
|
|
146
|
+
inputData?: string;
|
|
143
147
|
}
|
|
144
148
|
export interface CronSchedulerConfig {
|
|
145
149
|
/** Jobs persist path */
|
|
@@ -174,7 +178,7 @@ export declare class CronScheduler extends EventEmitter {
|
|
|
174
178
|
/**
|
|
175
179
|
* Set the task executor (for CronAgentBridge integration)
|
|
176
180
|
*/
|
|
177
|
-
setTaskExecutor(executor: (job: CronJob) => Promise<unknown>): void;
|
|
181
|
+
setTaskExecutor(executor: (job: CronJob, inputData?: string) => Promise<unknown>): void;
|
|
178
182
|
/**
|
|
179
183
|
* Load persisted jobs into memory without starting the tick loop. Useful for
|
|
180
184
|
* one-shot CLI commands (`buddy cron list/add/remove`) that must see the
|
|
@@ -182,7 +186,7 @@ export declare class CronScheduler extends EventEmitter {
|
|
|
182
186
|
* overwrite the file with only the in-memory jobs.
|
|
183
187
|
*/
|
|
184
188
|
loadFromDisk(): Promise<void>;
|
|
185
|
-
start(taskExecutor?: (job: CronJob) => Promise<unknown>): Promise<void>;
|
|
189
|
+
start(taskExecutor?: (job: CronJob, inputData?: string) => Promise<unknown>): Promise<void>;
|
|
186
190
|
stop(): Promise<void>;
|
|
187
191
|
/**
|
|
188
192
|
* Add a new job
|
|
@@ -232,11 +236,19 @@ export declare class CronScheduler extends EventEmitter {
|
|
|
232
236
|
/**
|
|
233
237
|
* Run a job immediately
|
|
234
238
|
*/
|
|
235
|
-
runJobNow(jobId: string): Promise<JobRun | null>;
|
|
239
|
+
runJobNow(jobId: string, inputData?: string): Promise<JobRun | null>;
|
|
236
240
|
private scheduleJob;
|
|
237
241
|
private cancelJobTimer;
|
|
238
242
|
private calculateNextRun;
|
|
239
243
|
private executeJob;
|
|
244
|
+
/** Max bytes for outputData passed between chained jobs. */
|
|
245
|
+
private static readonly MAX_OUTPUT_DATA_BYTES;
|
|
246
|
+
/**
|
|
247
|
+
* Extract a string `outputData` from the executor result. Looks for an
|
|
248
|
+
* explicit `outputData` field first, then falls back to `output`, then
|
|
249
|
+
* stringifies the whole result. Always capped at 64KB.
|
|
250
|
+
*/
|
|
251
|
+
static extractOutputData(result: unknown): string | undefined;
|
|
240
252
|
/**
|
|
241
253
|
* Resolve and execute a chained (`then`) job after its parent succeeds.
|
|
242
254
|
*
|
|
@@ -304,11 +304,11 @@ export class CronScheduler extends EventEmitter {
|
|
|
304
304
|
/**
|
|
305
305
|
* Run a job immediately
|
|
306
306
|
*/
|
|
307
|
-
async runJobNow(jobId) {
|
|
307
|
+
async runJobNow(jobId, inputData) {
|
|
308
308
|
const job = this.jobs.get(jobId);
|
|
309
309
|
if (!job)
|
|
310
310
|
return null;
|
|
311
|
-
return await this.executeJob(job);
|
|
311
|
+
return await this.executeJob(job, 0, inputData);
|
|
312
312
|
}
|
|
313
313
|
// ==========================================================================
|
|
314
314
|
// Scheduling
|
|
@@ -379,19 +379,20 @@ export class CronScheduler extends EventEmitter {
|
|
|
379
379
|
// ==========================================================================
|
|
380
380
|
// Execution
|
|
381
381
|
// ==========================================================================
|
|
382
|
-
async executeJob(job, chainDepth = 0) {
|
|
382
|
+
async executeJob(job, chainDepth = 0, inputData) {
|
|
383
383
|
const run = {
|
|
384
384
|
id: crypto.randomUUID(),
|
|
385
385
|
jobId: job.id,
|
|
386
386
|
startedAt: new Date(),
|
|
387
387
|
status: 'running',
|
|
388
|
+
...(inputData !== undefined ? { inputData } : {}),
|
|
388
389
|
};
|
|
389
390
|
this.emit('job:run:start', run);
|
|
390
391
|
try {
|
|
391
392
|
// Execute task
|
|
392
393
|
let result;
|
|
393
394
|
if (this.taskExecutor) {
|
|
394
|
-
result = await this.taskExecutor(job);
|
|
395
|
+
result = await this.taskExecutor(job, inputData);
|
|
395
396
|
}
|
|
396
397
|
else {
|
|
397
398
|
// Default execution (just log)
|
|
@@ -401,6 +402,10 @@ export class CronScheduler extends EventEmitter {
|
|
|
401
402
|
run.status = 'success';
|
|
402
403
|
run.result = result;
|
|
403
404
|
run.duration = run.completedAt.getTime() - run.startedAt.getTime();
|
|
405
|
+
// Extract outputData from the executor result for cross-job data passing.
|
|
406
|
+
// The executor may return an object with an `output` or `outputData`
|
|
407
|
+
// field; we cap at 64KB to keep run history manageable.
|
|
408
|
+
run.outputData = CronScheduler.extractOutputData(result);
|
|
404
409
|
job.lastRunAt = run.startedAt;
|
|
405
410
|
job.runCount++;
|
|
406
411
|
// Reset backoff on success
|
|
@@ -444,10 +449,50 @@ export class CronScheduler extends EventEmitter {
|
|
|
444
449
|
// successful run, so the chain runs inside a guarded helper that swallows
|
|
445
450
|
// (and logs) any error from the chained job.
|
|
446
451
|
if (run.status === 'success' && job.then) {
|
|
447
|
-
await this.runChainedJob(job, chainDepth);
|
|
452
|
+
await this.runChainedJob(job, chainDepth, run.outputData);
|
|
448
453
|
}
|
|
449
454
|
return run;
|
|
450
455
|
}
|
|
456
|
+
/** Max bytes for outputData passed between chained jobs. */
|
|
457
|
+
static MAX_OUTPUT_DATA_BYTES = 65_536; // 64 KiB
|
|
458
|
+
/**
|
|
459
|
+
* Extract a string `outputData` from the executor result. Looks for an
|
|
460
|
+
* explicit `outputData` field first, then falls back to `output`, then
|
|
461
|
+
* stringifies the whole result. Always capped at 64KB.
|
|
462
|
+
*/
|
|
463
|
+
static extractOutputData(result) {
|
|
464
|
+
if (result == null)
|
|
465
|
+
return undefined;
|
|
466
|
+
let raw;
|
|
467
|
+
if (typeof result === 'string') {
|
|
468
|
+
raw = result;
|
|
469
|
+
}
|
|
470
|
+
else if (typeof result === 'object') {
|
|
471
|
+
const obj = result;
|
|
472
|
+
if (typeof obj.outputData === 'string') {
|
|
473
|
+
raw = obj.outputData;
|
|
474
|
+
}
|
|
475
|
+
else if (typeof obj.output === 'string') {
|
|
476
|
+
raw = obj.output;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
try {
|
|
480
|
+
raw = JSON.stringify(result);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
raw = String(result);
|
|
489
|
+
}
|
|
490
|
+
if (!raw || raw.length === 0)
|
|
491
|
+
return undefined;
|
|
492
|
+
return raw.length > CronScheduler.MAX_OUTPUT_DATA_BYTES
|
|
493
|
+
? raw.slice(0, CronScheduler.MAX_OUTPUT_DATA_BYTES)
|
|
494
|
+
: raw;
|
|
495
|
+
}
|
|
451
496
|
/**
|
|
452
497
|
* Resolve and execute a chained (`then`) job after its parent succeeds.
|
|
453
498
|
*
|
|
@@ -455,7 +500,7 @@ export class CronScheduler extends EventEmitter {
|
|
|
455
500
|
* authored out of order. A missing/disabled target, a self-reference, or a
|
|
456
501
|
* depth-cap breach is logged and stops the chain — it never throws.
|
|
457
502
|
*/
|
|
458
|
-
async runChainedJob(parent, chainDepth) {
|
|
503
|
+
async runChainedJob(parent, chainDepth, outputData) {
|
|
459
504
|
const target = parent.then;
|
|
460
505
|
if (!target)
|
|
461
506
|
return;
|
|
@@ -493,7 +538,8 @@ export class CronScheduler extends EventEmitter {
|
|
|
493
538
|
return;
|
|
494
539
|
}
|
|
495
540
|
try {
|
|
496
|
-
|
|
541
|
+
// Pass the parent job's outputData as the chained job's inputData.
|
|
542
|
+
await this.executeJob(next, chainDepth + 1, outputData);
|
|
497
543
|
}
|
|
498
544
|
catch (err) {
|
|
499
545
|
// A chained failure is captured in the chained job's own run record; it
|
|
@@ -26,6 +26,8 @@ export interface CronScriptResult {
|
|
|
26
26
|
exitCode: number | null;
|
|
27
27
|
/** Combined stdout + stderr, capped. */
|
|
28
28
|
output: string;
|
|
29
|
+
/** Stdout only, capped at 64KB. Used for cross-job data passing. */
|
|
30
|
+
stdout: string;
|
|
29
31
|
/** True when the timeout fired and the process was killed. */
|
|
30
32
|
timedOut: boolean;
|
|
31
33
|
}
|
|
@@ -16,6 +16,7 @@ import { spawn } from 'child_process';
|
|
|
16
16
|
import path from 'path';
|
|
17
17
|
const DEFAULT_SCRIPT_TIMEOUT_MS = 600_000;
|
|
18
18
|
const MAX_OUTPUT_BYTES = 1_048_576; // 1 MiB cap on captured output
|
|
19
|
+
const MAX_STDOUT_BYTES = 65_536; // 64 KiB cap on stdout for data passing
|
|
19
20
|
const DEFAULT_ALLOWED_EXECUTABLES = new Set([
|
|
20
21
|
'bash',
|
|
21
22
|
'cargo',
|
|
@@ -68,27 +69,41 @@ function spawnScript(command, timeoutMs) {
|
|
|
68
69
|
windowsHide: true,
|
|
69
70
|
});
|
|
70
71
|
let output = '';
|
|
72
|
+
let stdoutOnly = '';
|
|
71
73
|
let timedOut = false;
|
|
72
|
-
const
|
|
74
|
+
const appendOutput = (chunk) => {
|
|
73
75
|
if (output.length < MAX_OUTPUT_BYTES) {
|
|
74
76
|
output += chunk;
|
|
75
77
|
}
|
|
76
78
|
};
|
|
79
|
+
const appendStdout = (chunk) => {
|
|
80
|
+
if (stdoutOnly.length < MAX_STDOUT_BYTES) {
|
|
81
|
+
stdoutOnly += chunk;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
77
84
|
const timer = setTimeout(() => {
|
|
78
85
|
timedOut = true;
|
|
79
86
|
child.kill('SIGTERM');
|
|
80
87
|
}, timeoutMs);
|
|
81
88
|
child.stdout.setEncoding('utf8');
|
|
82
|
-
child.stdout.on('data',
|
|
89
|
+
child.stdout.on('data', (chunk) => {
|
|
90
|
+
appendOutput(chunk);
|
|
91
|
+
appendStdout(chunk);
|
|
92
|
+
});
|
|
83
93
|
child.stderr.setEncoding('utf8');
|
|
84
|
-
child.stderr.on('data',
|
|
94
|
+
child.stderr.on('data', appendOutput);
|
|
85
95
|
child.on('error', (error) => {
|
|
86
96
|
clearTimeout(timer);
|
|
87
97
|
reject(error);
|
|
88
98
|
});
|
|
89
99
|
child.on('close', (exitCode) => {
|
|
90
100
|
clearTimeout(timer);
|
|
91
|
-
resolve({
|
|
101
|
+
resolve({
|
|
102
|
+
exitCode,
|
|
103
|
+
output: output.trimEnd(),
|
|
104
|
+
stdout: stdoutOnly.trimEnd(),
|
|
105
|
+
timedOut,
|
|
106
|
+
});
|
|
92
107
|
});
|
|
93
108
|
});
|
|
94
109
|
}
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* The aggregate `ok` is true only when there are no alerts and no errors.
|
|
17
17
|
*/
|
|
18
18
|
import { spawn } from 'child_process';
|
|
19
|
-
import fs from 'fs';
|
|
20
19
|
import path from 'path';
|
|
20
|
+
import { getFreeSpaceInfo } from '../utils/disk-guard.js';
|
|
21
21
|
const DEFAULT_HTTP_TIMEOUT_MS = 10_000;
|
|
22
22
|
const DEFAULT_BUILD_ALLOWED_EXECUTABLES = new Set([
|
|
23
23
|
'bash',
|
|
@@ -115,32 +115,19 @@ async function runSingleCheck(check, allowedExecutables) {
|
|
|
115
115
|
}
|
|
116
116
|
function checkDisk(check, name) {
|
|
117
117
|
const target = check.path || process.cwd();
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
// Single source of truth for free space (uses bavail); null when statfs is
|
|
119
|
+
// unavailable or the path can't be stat'd.
|
|
120
|
+
const info = getFreeSpaceInfo(target);
|
|
121
|
+
if (info === null) {
|
|
120
122
|
return {
|
|
121
123
|
type: 'disk',
|
|
122
124
|
name,
|
|
123
125
|
status: 'error',
|
|
124
|
-
summary:
|
|
126
|
+
summary: `cannot determine free space at ${target} (fs.statfsSync unavailable or path unreadable)`,
|
|
125
127
|
details: { path: target },
|
|
126
128
|
};
|
|
127
129
|
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
stats = statfs(target);
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
return {
|
|
134
|
-
type: 'disk',
|
|
135
|
-
name,
|
|
136
|
-
status: 'error',
|
|
137
|
-
summary: `cannot stat filesystem at ${target}: ${errMsg(err)}`,
|
|
138
|
-
details: { path: target },
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
const totalBytes = stats.blocks * stats.bsize;
|
|
142
|
-
const freeBytes = stats.bavail * stats.bsize;
|
|
143
|
-
const freePercent = totalBytes > 0 ? (freeBytes / totalBytes) * 100 : 0;
|
|
130
|
+
const { freeBytes, totalBytes, freePercent } = info;
|
|
144
131
|
const breaches = [];
|
|
145
132
|
if (typeof check.minFreeBytes === 'number' && freeBytes < check.minFreeBytes) {
|
|
146
133
|
breaches.push(`free ${formatBytes(freeBytes)} < min ${formatBytes(check.minFreeBytes)}`);
|
|
@@ -89,8 +89,13 @@ export class USearchVectorIndex extends EventEmitter {
|
|
|
89
89
|
*/
|
|
90
90
|
async loadUSearch() {
|
|
91
91
|
try {
|
|
92
|
-
// Dynamic import for
|
|
93
|
-
|
|
92
|
+
// Dynamic import for an OPTIONAL native dependency (see optionalDependencies).
|
|
93
|
+
// Use a non-literal specifier so `tsc` doesn't require 'usearch' to be
|
|
94
|
+
// resolvable on every platform — it isn't installed on some CI runners
|
|
95
|
+
// (e.g. windows-latest), which otherwise fails type-check with TS2307.
|
|
96
|
+
// Types come from the local USearchModule interface, not the package.
|
|
97
|
+
const usearchSpecifier = 'usearch';
|
|
98
|
+
const usearch = (await import(usearchSpecifier));
|
|
94
99
|
return usearch;
|
|
95
100
|
}
|
|
96
101
|
catch (_err) {
|
|
@@ -42,6 +42,8 @@ export const TOOL_GROUPS = {
|
|
|
42
42
|
// Runtime - Process
|
|
43
43
|
spawn_process: ['group:runtime', 'group:runtime:process'],
|
|
44
44
|
kill_process: ['group:runtime', 'group:runtime:process', 'group:dangerous'],
|
|
45
|
+
// Runtime - Office macros (arbitrary VBA/PowerShell via COM, Windows only)
|
|
46
|
+
office_macro_execute: ['group:runtime', 'group:runtime:shell', 'group:dangerous'],
|
|
45
47
|
// Web - Fetch
|
|
46
48
|
web_fetch: ['group:web', 'group:web:fetch'],
|
|
47
49
|
http_request: ['group:web', 'group:web:fetch'],
|
package/dist/server/index.js
CHANGED
|
@@ -14,6 +14,8 @@ import { createRequire } from 'module';
|
|
|
14
14
|
import express from 'express';
|
|
15
15
|
import cors from 'cors';
|
|
16
16
|
import { createServer } from 'http';
|
|
17
|
+
import { createServer as createHttpsServer, Server as HttpsServer } from 'https';
|
|
18
|
+
import { resolveServerTlsOptions } from './tls-config.js';
|
|
17
19
|
const _require = createRequire(import.meta.url);
|
|
18
20
|
let SERVER_VERSION = '0.0.0';
|
|
19
21
|
try {
|
|
@@ -77,7 +79,10 @@ export function getServerBaseUrl(server, config) {
|
|
|
77
79
|
const port = typeof address === 'object' && address ? address.port : config.port;
|
|
78
80
|
const publicHost = config.host === '0.0.0.0' || config.host === '::' ? '127.0.0.1' : config.host;
|
|
79
81
|
const host = publicHost.includes(':') && !publicHost.startsWith('[') ? `[${publicHost}]` : publicHost;
|
|
80
|
-
|
|
82
|
+
// Report https:// when the server was constructed with TLS so the logged URL
|
|
83
|
+
// isn't a lie. Default (plain http.Server) stays http://.
|
|
84
|
+
const scheme = server instanceof HttpsServer ? 'https' : 'http';
|
|
85
|
+
return `${scheme}://${host}:${port}`;
|
|
81
86
|
}
|
|
82
87
|
// Default configuration
|
|
83
88
|
const DEFAULT_CONFIG = {
|
|
@@ -878,7 +883,17 @@ export async function startServer(userConfig = {}) {
|
|
|
878
883
|
/* prometheus exporter optional */
|
|
879
884
|
}
|
|
880
885
|
const app = createApp(config);
|
|
881
|
-
|
|
886
|
+
// Optional TLS: serve the API (including /api/mobile) over HTTPS when
|
|
887
|
+
// CODEBUDDY_HTTPS / CODEBUDDY_MOBILE_TLS is set so the mobile-supervision
|
|
888
|
+
// endpoint can be exposed off-device securely. Default (no env) is plain HTTP,
|
|
889
|
+
// byte-for-byte unchanged. resolveServerTlsOptions() throws (never silently
|
|
890
|
+
// downgrades) if TLS is requested but cannot be satisfied. The https.Server
|
|
891
|
+
// surface (listen/address/on('upgrade')/close) matches http.Server at runtime,
|
|
892
|
+
// so we keep the `HttpServer` type to avoid widening downstream signatures.
|
|
893
|
+
const tlsOptions = resolveServerTlsOptions();
|
|
894
|
+
const server = tlsOptions
|
|
895
|
+
? createHttpsServer(tlsOptions, app)
|
|
896
|
+
: createServer(app);
|
|
882
897
|
const startChannelBridge = async (baseUrl) => {
|
|
883
898
|
try {
|
|
884
899
|
const { getChannelManager } = await import('../channels/index.js');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
2
3
|
export declare const mobileRouter: import("express-serve-static-core").Router;
|
|
3
4
|
export declare let activePairingCode: string;
|
|
4
5
|
export declare let activePairingCodeExpiresAt: number;
|
|
@@ -10,11 +11,11 @@ export declare const activeTokens: Map<string, {
|
|
|
10
11
|
* A follow-up prompt review draft. The mobile device (or a draft-only call) can
|
|
11
12
|
* *propose* a prompt, but it never executes: it lands here as
|
|
12
13
|
* `needs_local_operator` and only a local operator (loopback) can `approve` or
|
|
13
|
-
* `cancel` it.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* `cancel` it. When `CODEBUDDY_MOBILE_ALLOW_DISPATCH=true`, an approved draft
|
|
15
|
+
* can optionally be dispatched to the agent for execution. When the env var is
|
|
16
|
+
* OFF (default), approval remains a review-gate marker only.
|
|
16
17
|
*/
|
|
17
|
-
export type FollowupDraftStatus = 'needs_local_operator' | 'approved' | 'cancelled';
|
|
18
|
+
export type FollowupDraftStatus = 'needs_local_operator' | 'approved' | 'cancelled' | 'dispatched' | 'completed' | 'failed';
|
|
18
19
|
export interface FollowupDraft {
|
|
19
20
|
id: string;
|
|
20
21
|
prompt: string;
|
|
@@ -24,9 +25,16 @@ export interface FollowupDraft {
|
|
|
24
25
|
approvedBy?: string;
|
|
25
26
|
approvedAt?: number;
|
|
26
27
|
cancelledAt?: number;
|
|
28
|
+
dispatchedAt?: number;
|
|
29
|
+
completedAt?: number;
|
|
30
|
+
failedAt?: number;
|
|
31
|
+
failureReason?: string;
|
|
27
32
|
[key: string]: unknown;
|
|
28
33
|
}
|
|
29
34
|
export declare const followupDrafts: FollowupDraft[];
|
|
35
|
+
export declare const mobileEventBus: EventEmitter<[never]>;
|
|
36
|
+
/** Check whether dispatch is enabled via environment variable. */
|
|
37
|
+
export declare function isDispatchEnabled(): boolean;
|
|
30
38
|
/**
|
|
31
39
|
* Decide whether a request originates from the loopback interface. We read the
|
|
32
40
|
* raw socket address (`req.socket.remoteAddress`) on purpose: `req.ip` and the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import fs from 'fs';
|
|
5
6
|
import { logger } from '../../utils/logger.js';
|
|
@@ -36,6 +37,11 @@ function rotatePairingCode() {
|
|
|
36
37
|
}
|
|
37
38
|
activePairingCode = nextCode;
|
|
38
39
|
activePairingCodeExpiresAt = Date.now() + getPairingCodeTtlMs();
|
|
40
|
+
mobileEventBus.emit('event', {
|
|
41
|
+
type: 'pairing_rotated',
|
|
42
|
+
pairingCodeExpiresAt: activePairingCodeExpiresAt,
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
});
|
|
39
45
|
return activePairingCode;
|
|
40
46
|
}
|
|
41
47
|
function ensureActivePairingCodeFresh() {
|
|
@@ -44,6 +50,15 @@ function ensureActivePairingCodeFresh() {
|
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
export const followupDrafts = [];
|
|
53
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// Mobile event bus — emits SSE-pushed events for mobile clients.
|
|
55
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
56
|
+
export const mobileEventBus = new EventEmitter();
|
|
57
|
+
mobileEventBus.setMaxListeners(50);
|
|
58
|
+
/** Check whether dispatch is enabled via environment variable. */
|
|
59
|
+
export function isDispatchEnabled() {
|
|
60
|
+
return process.env.CODEBUDDY_MOBILE_ALLOW_DISPATCH === 'true';
|
|
61
|
+
}
|
|
47
62
|
function pruneExpiredTokens(now = Date.now()) {
|
|
48
63
|
for (const [token, tokenData] of activeTokens) {
|
|
49
64
|
if (now > tokenData.expiresAt) {
|
|
@@ -158,7 +173,8 @@ mobileRouter.get('/followup-drafts', loopbackOnlyMiddleware, (req, res) => {
|
|
|
158
173
|
res.json({ ok: true, drafts: followupDrafts });
|
|
159
174
|
});
|
|
160
175
|
// POST /api/mobile/followup-draft/:id/approve: local operator approves a draft.
|
|
161
|
-
//
|
|
176
|
+
// When CODEBUDDY_MOBILE_ALLOW_DISPATCH is OFF (default), approval is a
|
|
177
|
+
// review-gate marker only — it never dispatches or executes work.
|
|
162
178
|
mobileRouter.post('/followup-draft/:id/approve', loopbackOnlyMiddleware, (req, res) => {
|
|
163
179
|
const draft = followupDrafts.find((d) => d.id === req.params.id);
|
|
164
180
|
if (!draft) {
|
|
@@ -175,6 +191,12 @@ mobileRouter.post('/followup-draft/:id/approve', loopbackOnlyMiddleware, (req, r
|
|
|
175
191
|
draft.status = 'approved';
|
|
176
192
|
draft.approvedBy = reviewer;
|
|
177
193
|
draft.approvedAt = Date.now();
|
|
194
|
+
mobileEventBus.emit('event', {
|
|
195
|
+
type: 'draft_status_changed',
|
|
196
|
+
draftId: draft.id,
|
|
197
|
+
status: draft.status,
|
|
198
|
+
timestamp: Date.now(),
|
|
199
|
+
});
|
|
178
200
|
res.json({
|
|
179
201
|
ok: true,
|
|
180
202
|
message: 'Draft approved by local operator. No work is dispatched automatically — execution stays local and explicit.',
|
|
@@ -194,8 +216,52 @@ mobileRouter.post('/followup-draft/:id/cancel', loopbackOnlyMiddleware, (req, re
|
|
|
194
216
|
}
|
|
195
217
|
draft.status = 'cancelled';
|
|
196
218
|
draft.cancelledAt = Date.now();
|
|
219
|
+
mobileEventBus.emit('event', {
|
|
220
|
+
type: 'draft_status_changed',
|
|
221
|
+
draftId: draft.id,
|
|
222
|
+
status: draft.status,
|
|
223
|
+
timestamp: Date.now(),
|
|
224
|
+
});
|
|
197
225
|
res.json({ ok: true, message: 'Draft cancelled by local operator.', draft });
|
|
198
226
|
});
|
|
227
|
+
// POST /api/mobile/followup-draft/:id/dispatch: local operator dispatches an
|
|
228
|
+
// approved draft to the agent. Gated behind CODEBUDDY_MOBILE_ALLOW_DISPATCH.
|
|
229
|
+
mobileRouter.post('/followup-draft/:id/dispatch', loopbackOnlyMiddleware, (req, res) => {
|
|
230
|
+
if (!isDispatchEnabled()) {
|
|
231
|
+
res.status(403).json({
|
|
232
|
+
ok: false,
|
|
233
|
+
error: 'Dispatch is disabled. Set CODEBUDDY_MOBILE_ALLOW_DISPATCH=true to enable.',
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const draft = followupDrafts.find((d) => d.id === req.params.id);
|
|
238
|
+
if (!draft) {
|
|
239
|
+
res.status(404).json({ ok: false, error: 'Draft not found' });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (draft.status !== 'approved') {
|
|
243
|
+
res.status(409).json({
|
|
244
|
+
ok: false,
|
|
245
|
+
error: `Draft must be approved before dispatch (status: ${draft.status})`,
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
draft.status = 'dispatched';
|
|
250
|
+
draft.dispatchedAt = Date.now();
|
|
251
|
+
mobileEventBus.emit('event', {
|
|
252
|
+
type: 'draft_status_changed',
|
|
253
|
+
draftId: draft.id,
|
|
254
|
+
status: draft.status,
|
|
255
|
+
timestamp: Date.now(),
|
|
256
|
+
});
|
|
257
|
+
// Emit a dispatch queue entry for the agent to pick up.
|
|
258
|
+
mobileEventBus.emit('dispatch', { draftId: draft.id, prompt: draft.prompt });
|
|
259
|
+
res.json({
|
|
260
|
+
ok: true,
|
|
261
|
+
message: 'Draft dispatched to agent for execution.',
|
|
262
|
+
draft,
|
|
263
|
+
});
|
|
264
|
+
});
|
|
199
265
|
// 1. POST /api/mobile/pair: Validate pairing request and issue short-lived token
|
|
200
266
|
mobileRouter.post('/pair', (req, res) => {
|
|
201
267
|
const code = typeof req.body?.code === 'string' ? req.body.code.trim() : '';
|
|
@@ -221,6 +287,49 @@ mobileRouter.post('/pair', (req, res) => {
|
|
|
221
287
|
expiresAt,
|
|
222
288
|
});
|
|
223
289
|
});
|
|
290
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
291
|
+
// SSE push endpoint — authenticated via mobile bearer token (query param
|
|
292
|
+
// fallback for EventSource API which doesn't support Authorization header).
|
|
293
|
+
// Pushes: draft_status_changed, snapshot_available, pairing_rotated.
|
|
294
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
295
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
296
|
+
mobileRouter.get('/events', (req, res) => {
|
|
297
|
+
// Auth: prefer Authorization header, fall back to ?token= query param.
|
|
298
|
+
const authHeader = req.headers.authorization;
|
|
299
|
+
let token;
|
|
300
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
301
|
+
token = authHeader.substring(7).trim();
|
|
302
|
+
}
|
|
303
|
+
else if (typeof req.query.token === 'string') {
|
|
304
|
+
token = req.query.token.trim();
|
|
305
|
+
}
|
|
306
|
+
if (!token || !isValidToken(token)) {
|
|
307
|
+
res.status(401).json({ ok: false, error: 'Unauthorized: Invalid or expired pairing token' });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// SSE headers.
|
|
311
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
312
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
313
|
+
res.setHeader('Connection', 'keep-alive');
|
|
314
|
+
res.flushHeaders();
|
|
315
|
+
const sendSSE = (payload) => {
|
|
316
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
317
|
+
};
|
|
318
|
+
// Heartbeat to keep the connection alive.
|
|
319
|
+
const heartbeat = setInterval(() => {
|
|
320
|
+
sendSSE({ type: 'heartbeat', timestamp: Date.now() });
|
|
321
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
322
|
+
// Forward bus events to this SSE client.
|
|
323
|
+
const onEvent = (payload) => {
|
|
324
|
+
sendSSE(payload);
|
|
325
|
+
};
|
|
326
|
+
mobileEventBus.on('event', onEvent);
|
|
327
|
+
// Cleanup on disconnect.
|
|
328
|
+
req.on('close', () => {
|
|
329
|
+
clearInterval(heartbeat);
|
|
330
|
+
mobileEventBus.off('event', onEvent);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
224
333
|
// Apply mobile auth middleware to all remaining routes
|
|
225
334
|
mobileRouter.use(mobileAuthMiddleware);
|
|
226
335
|
// 2. GET /api/mobile/snapshot: Return active run snapshot summary
|
|
@@ -331,6 +440,12 @@ async function enqueuePendingFollowupDraft(prompt, query, source) {
|
|
|
331
440
|
createdAt: Date.now(),
|
|
332
441
|
};
|
|
333
442
|
followupDrafts.push(savedDraft);
|
|
443
|
+
mobileEventBus.emit('event', {
|
|
444
|
+
type: 'draft_status_changed',
|
|
445
|
+
draftId: savedDraft.id,
|
|
446
|
+
status: savedDraft.status,
|
|
447
|
+
timestamp: Date.now(),
|
|
448
|
+
});
|
|
334
449
|
return savedDraft;
|
|
335
450
|
}
|
|
336
451
|
// 5. POST /api/mobile/followup-draft: Save a local operator followup review draft
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional TLS for the API server.
|
|
3
|
+
*
|
|
4
|
+
* The mobile-supervision endpoint (`/api/mobile`) rides the main HTTP server.
|
|
5
|
+
* To expose it off-device securely it can be served over HTTPS. This is purely
|
|
6
|
+
* transport security — it does not change any product gate (the dispatch gate
|
|
7
|
+
* `CODEBUDDY_MOBILE_ALLOW_DISPATCH` is untouched).
|
|
8
|
+
*
|
|
9
|
+
* Behaviour, gated by env (HTTP stays the default — no env, no TLS):
|
|
10
|
+
* - `CODEBUDDY_HTTPS=1` (or `CODEBUDDY_MOBILE_TLS=1`) enables HTTPS.
|
|
11
|
+
* - `CODEBUDDY_TLS_CERT` / `CODEBUDDY_TLS_KEY` = paths to a PEM cert/key. This
|
|
12
|
+
* is the standard production approach (Let's Encrypt / reverse proxy / the
|
|
13
|
+
* operator provisions the cert).
|
|
14
|
+
* - If TLS is enabled but no cert is provided, a dev self-signed cert is
|
|
15
|
+
* generated via `openssl` (if on PATH) into `~/.codebuddy/tls/` and reused.
|
|
16
|
+
* - If TLS is enabled but it cannot be satisfied (cert unreadable, or openssl
|
|
17
|
+
* absent), this **throws** with a clear message. It never silently falls back
|
|
18
|
+
* to plain HTTP — an operator who asked for TLS must not be downgraded.
|
|
19
|
+
*
|
|
20
|
+
* Uses only Node built-ins (`https`, `fs`, `child_process`) — no new dependency.
|
|
21
|
+
*/
|
|
22
|
+
export interface ServerTlsOptions {
|
|
23
|
+
cert: string | Buffer;
|
|
24
|
+
key: string | Buffer;
|
|
25
|
+
}
|
|
26
|
+
/** Whether HTTPS is requested for the API server. */
|
|
27
|
+
export declare function isTlsEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve TLS options for the server, or `null` when TLS is not requested.
|
|
30
|
+
*
|
|
31
|
+
* Returns `null` (HTTP, unchanged default) when neither `CODEBUDDY_HTTPS` nor
|
|
32
|
+
* `CODEBUDDY_MOBILE_TLS` is set. Throws a clear error if TLS is requested but
|
|
33
|
+
* cannot be satisfied — it never downgrades to HTTP behind the operator's back.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveServerTlsOptions(env?: NodeJS.ProcessEnv): ServerTlsOptions | null;
|