offsite 0.0.2 → 0.0.4

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.
Files changed (114) hide show
  1. package/README.md +18 -7
  2. package/apps/server/dist/main.mjs +5767 -0
  3. package/apps/server/dist/main.mjs.map +1 -0
  4. package/apps/web/dist/assets/{angular-html-Oa9dnZbT.js → angular-html-prQpjz7s.js} +1 -1
  5. package/apps/web/dist/assets/{angular-ts-F_BIbY2H.js → angular-ts-BE4HJ5Uh.js} +1 -1
  6. package/apps/web/dist/assets/{apl-D7AuiKiV.js → apl-CJ-NIQ-7.js} +1 -1
  7. package/apps/web/dist/assets/{astro-DnpymQyW.js → astro-CVkyXRVY.js} +1 -1
  8. package/apps/web/dist/assets/{blade-CZZrW0iI.js → blade-CQFMHse_.js} +1 -1
  9. package/apps/web/dist/assets/{c-V_Rix0w2.js → c-gqNWYRL8.js} +1 -1
  10. package/apps/web/dist/assets/{chapel-fGtLLMYq.js → chapel-CQ9_BV4j.js} +1 -1
  11. package/apps/web/dist/assets/{cobol-BFOTnbFa.js → cobol-D-9v184m.js} +1 -1
  12. package/apps/web/dist/assets/{coffee-BdvfipEg.js → coffee-BE0tIOLL.js} +1 -1
  13. package/apps/web/dist/assets/{cpp-D_NFR3H2.js → cpp-CzOLC-NF.js} +1 -1
  14. package/apps/web/dist/assets/{crystal-BM2vkBPa.js → crystal-f1GcBNi_.js} +1 -1
  15. package/apps/web/dist/assets/{css-Byur6C5q.js → css-DlqRAumc.js} +1 -1
  16. package/apps/web/dist/assets/{edge-CP-KL2DQ.js → edge-DQkkPJaW.js} +1 -1
  17. package/apps/web/dist/assets/{elixir-CiO075Fh.js → elixir-ClG78o74.js} +1 -1
  18. package/apps/web/dist/assets/{elm-DIyG-d95.js → elm-DZDmCmuw.js} +1 -1
  19. package/apps/web/dist/assets/{erb-DHZK6WKc.js → erb-B1mu0-DN.js} +1 -1
  20. package/apps/web/dist/assets/{git-rebase-CxXKbwiy.js → git-rebase-x1gs-rK2.js} +1 -1
  21. package/apps/web/dist/assets/{glimmer-js-BUhQC_gD.js → glimmer-js-Cjos1igW.js} +1 -1
  22. package/apps/web/dist/assets/{glimmer-ts-D2lLByyw.js → glimmer-ts-CrR-PFvn.js} +1 -1
  23. package/apps/web/dist/assets/{glsl-BMvlWqE_.js → glsl-DR0G8e99.js} +1 -1
  24. package/apps/web/dist/assets/{graphql-D9UFk_fJ.js → graphql-DKmrBZt7.js} +1 -1
  25. package/apps/web/dist/assets/{hack-BZAO0Jc2.js → hack-Brs_j9Xv.js} +1 -1
  26. package/apps/web/dist/assets/{haml-Dm5wroII.js → haml-8Lc-lF96.js} +1 -1
  27. package/apps/web/dist/assets/{handlebars-C9L1Bn7f.js → handlebars-Ds8WdPdv.js} +1 -1
  28. package/apps/web/dist/assets/{html-Bz91Luf0.js → html-C0gvYDho.js} +1 -1
  29. package/apps/web/dist/assets/{html-derivative-hIdjVrpA.js → html-derivative-CU9dkErg.js} +1 -1
  30. package/apps/web/dist/assets/{http-AWG6aeFM.js → http-BE0w0Fty.js} +1 -1
  31. package/apps/web/dist/assets/{hurl-pSGcpqgh.js → hurl-DqZk6YcW.js} +1 -1
  32. package/apps/web/dist/assets/index-0B-Gdd7b.css +2 -0
  33. package/apps/web/dist/assets/index-rgKS_aMw.js +1148 -0
  34. package/apps/web/dist/assets/{java-D_CyC-X4.js → java-PSSQ0YmM.js} +1 -1
  35. package/apps/web/dist/assets/{javascript-PK4jkG9t.js → javascript-Ci3r4rzs.js} +1 -1
  36. package/apps/web/dist/assets/{jinja-Cu3wzgkz.js → jinja-lLTko9Wl.js} +1 -1
  37. package/apps/web/dist/assets/{jison-CHevE1W8.js → jison-1BpHD5UK.js} +1 -1
  38. package/apps/web/dist/assets/{json-DPTkd0bP.js → json-BAUn76M4.js} +1 -1
  39. package/apps/web/dist/assets/{jsx-DcsDzCOA.js → jsx-B8sFp1vz.js} +1 -1
  40. package/apps/web/dist/assets/{julia-B9tMVOL0.js → julia-C4TZ4EL9.js} +1 -1
  41. package/apps/web/dist/assets/{just-BHIhrE9N.js → just-D5qIrqkp.js} +1 -1
  42. package/apps/web/dist/assets/{latex-CY05HK-G.js → latex-BL7XYxFg.js} +1 -1
  43. package/apps/web/dist/assets/{liquid-DJaL8XK2.js → liquid-DBx9Hq7B.js} +1 -1
  44. package/apps/web/dist/assets/{lua-BfaWUinJ.js → lua-BqjicQyd.js} +1 -1
  45. package/apps/web/dist/assets/{marko-0Lc0z7w6.js → marko-CHgy7yaY.js} +1 -1
  46. package/apps/web/dist/assets/{mdc-DEhXNYDZ.js → mdc-DOloBwb8.js} +1 -1
  47. package/apps/web/dist/assets/{nginx-DLuBebI5.js → nginx-g7I1HPwV.js} +1 -1
  48. package/apps/web/dist/assets/{nim-BIJh7lZB.js → nim-O6zJV1LD.js} +1 -1
  49. package/apps/web/dist/assets/{org-BFpxb2WJ.js → org-HLg4-757.js} +1 -1
  50. package/apps/web/dist/assets/{perl-DHbDMq2q.js → perl-B3cDKTai.js} +1 -1
  51. package/apps/web/dist/assets/{php-BcVbiJpZ.js → php-2eDApoU3.js} +1 -1
  52. package/apps/web/dist/assets/{pug-bdfyK-rS.js → pug-itzGn-nK.js} +1 -1
  53. package/apps/web/dist/assets/{qml-BC2D8eWk.js → qml-sU0VtUmf.js} +1 -1
  54. package/apps/web/dist/assets/{r-O3zbZ-YT.js → r-C6mhPqgr.js} +1 -1
  55. package/apps/web/dist/assets/{razor-Ffnu5_vD.js → razor-CSzRtbW7.js} +1 -1
  56. package/apps/web/dist/assets/{regexp-dnbUAgjx.js → regexp-BmdBJRV9.js} +1 -1
  57. package/apps/web/dist/assets/{rst-DJfy2dSm.js → rst-BRxUd6sp.js} +1 -1
  58. package/apps/web/dist/assets/{ruby-DGEpA0mD.js → ruby-CQHHOfmX.js} +1 -1
  59. package/apps/web/dist/assets/{sas-CZvL8X8k.js → sas-DlaL8b25.js} +1 -1
  60. package/apps/web/dist/assets/{scss-BtNSCzZB.js → scss-Cc71ABWp.js} +1 -1
  61. package/apps/web/dist/assets/{shellscript-uR-sOZlr.js → shellscript-CzA_LIIm.js} +1 -1
  62. package/apps/web/dist/assets/{shellsession-BT-Jl4S5.js → shellsession-DESJlgp3.js} +1 -1
  63. package/apps/web/dist/assets/{soy-BjfL_vrj.js → soy-Nkvq88YN.js} +1 -1
  64. package/apps/web/dist/assets/{sql-fa1gAeRF.js → sql-CtPxrO7T.js} +1 -1
  65. package/apps/web/dist/assets/{stata-DgTOpGhg.js → stata-BacEfVTr.js} +1 -1
  66. package/apps/web/dist/assets/{surrealql-DVqEkJeu.js → surrealql-CYW10JNe.js} +1 -1
  67. package/apps/web/dist/assets/{svelte-CyQ5sU9I.js → svelte-D3_tCKGs.js} +1 -1
  68. package/apps/web/dist/assets/{templ-DrztMbAf.js → templ-BGKIfY_b.js} +1 -1
  69. package/apps/web/dist/assets/{tex-NAwM8Kkh.js → tex-BkkYOaXX.js} +1 -1
  70. package/apps/web/dist/assets/{ts-tags-DX96dDxh.js → ts-tags-B02tnbpZ.js} +1 -1
  71. package/apps/web/dist/assets/{tsx-D3PnhDHK.js → tsx-D3MB1IUf.js} +1 -1
  72. package/apps/web/dist/assets/{twig-FG0P9evf.js → twig-CPhw2wGK.js} +1 -1
  73. package/apps/web/dist/assets/{typescript-Crqeuw0P.js → typescript-B7U4faGm.js} +1 -1
  74. package/apps/web/dist/assets/{typst-CO_2MFRr.js → typst-CN9EZswy.js} +1 -1
  75. package/apps/web/dist/assets/{vue-fTNrS0Q3.js → vue-YH8dfjo_.js} +1 -1
  76. package/apps/web/dist/assets/{vue-html-E0ptmXwc.js → vue-html-NwX2xzgp.js} +1 -1
  77. package/apps/web/dist/assets/{vue-vine-C34_rJIu.js → vue-vine-DoRmnjLj.js} +1 -1
  78. package/apps/web/dist/assets/{xml-BB9WbMMU.js → xml-DgfgLpPc.js} +1 -1
  79. package/apps/web/dist/assets/{xsl-B3w2P7LL.js → xsl-CEwQRUK_.js} +1 -1
  80. package/apps/web/dist/assets/{yaml-BKGRKqBA.js → yaml-CEFOzF95.js} +1 -1
  81. package/apps/web/dist/icon.svg +5 -0
  82. package/apps/web/dist/index.html +2 -2
  83. package/apps/web/dist/sw.js +19 -8
  84. package/bin/offsite.js +1 -1
  85. package/package.json +6 -1
  86. package/apps/server/dist/args.js +0 -11
  87. package/apps/server/dist/claude.js +0 -207
  88. package/apps/server/dist/config.js +0 -185
  89. package/apps/server/dist/content.js +0 -172
  90. package/apps/server/dist/db.js +0 -13
  91. package/apps/server/dist/files.js +0 -50
  92. package/apps/server/dist/git.js +0 -133
  93. package/apps/server/dist/history.js +0 -137
  94. package/apps/server/dist/hub.js +0 -10
  95. package/apps/server/dist/main.js +0 -64
  96. package/apps/server/dist/projects.js +0 -92
  97. package/apps/server/dist/protocol.js +0 -20
  98. package/apps/server/dist/push.js +0 -152
  99. package/apps/server/dist/run.js +0 -375
  100. package/apps/server/dist/runs.js +0 -102
  101. package/apps/server/dist/schemes.js +0 -262
  102. package/apps/server/dist/sessions.js +0 -65
  103. package/apps/server/dist/socket.js +0 -149
  104. package/apps/server/dist/storage.js +0 -22
  105. package/apps/server/dist/terminal/alerts.js +0 -145
  106. package/apps/server/dist/terminal/process.js +0 -207
  107. package/apps/server/dist/terminal/shell-check.js +0 -91
  108. package/apps/server/dist/terminal/shell-integration.js +0 -112
  109. package/apps/server/dist/terminal/terminal.js +0 -402
  110. package/apps/web/dist/assets/index-BiLbXfDN.js +0 -899
  111. package/apps/web/dist/assets/index-DQIK-TpZ.css +0 -2
  112. /package/apps/server/src/{terminal → services/terminal}/shell-integration/bash/offsite.bash +0 -0
  113. /package/apps/server/src/{terminal → services/terminal}/shell-integration/fish/offsite.fish +0 -0
  114. /package/apps/server/src/{terminal → services/terminal}/shell-integration/zsh/.zshenv +0 -0
@@ -1,20 +0,0 @@
1
- export const FRAME_INPUT = 0;
2
- export const FRAME_REPLY = 1;
3
- export const FRAME_OUTPUT = 2;
4
- export const FRAME_REPLAY = 3;
5
- export function encodeFrame(kind, id, data) {
6
- const frame = new Uint8Array(5 + data.length);
7
- frame[0] = kind;
8
- new DataView(frame.buffer).setUint32(1, id);
9
- frame.set(data, 5);
10
- return frame;
11
- }
12
- export function decodeFrame(frame) {
13
- if (frame.length < 5)
14
- throw new Error('terminal frame is too short');
15
- return {
16
- kind: frame[0],
17
- id: new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(1),
18
- data: frame.subarray(5),
19
- };
20
- }
@@ -1,152 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { renameSync } from 'node:fs';
3
- import path from 'node:path';
4
- import webPush from 'web-push';
5
- import { home, readJson, writeJson } from './storage.js';
6
- const pushFile = path.join(home, 'push.json');
7
- const TITLE_LIMIT = 100;
8
- const BODY_LIMIT = 500;
9
- const TIME_TO_LIVE = 15 * 60;
10
- const SEND_TIMEOUT = 10_000;
11
- const SUBJECT = 'https://github.com/NazmusSayad/offsite';
12
- let server = '';
13
- let active = false;
14
- export function startPush(address) {
15
- server = address;
16
- try {
17
- const store = readJson('push.json');
18
- if (store !== null)
19
- track(store);
20
- }
21
- catch (error) {
22
- console.warn(`push notifications are off until a browser turns them on again, could not read ${pushFile}: ${error}`);
23
- }
24
- }
25
- export function pushActive() {
26
- return active;
27
- }
28
- export function pushKey() {
29
- return { publicKey: read().publicKey };
30
- }
31
- export function subscribe(subscription) {
32
- const endpoint = new URL(subscription.endpoint);
33
- if (endpoint.protocol !== 'https:')
34
- throw new Error('the endpoint must be an https URL');
35
- update((store) => {
36
- store.subscriptions = store.subscriptions.filter((existing) => existing.endpoint !== subscription.endpoint);
37
- store.subscriptions.push({
38
- endpoint: subscription.endpoint,
39
- keys: { p256dh: subscription.keys.p256dh, auth: subscription.keys.auth },
40
- server,
41
- });
42
- });
43
- console.log('a browser subscribed to push notifications');
44
- }
45
- export function unsubscribe(endpoint) {
46
- update((store) => {
47
- store.subscriptions = store.subscriptions.filter((existing) => existing.endpoint !== endpoint);
48
- });
49
- console.log('a browser unsubscribed from push notifications');
50
- }
51
- export async function sendPush(message) {
52
- let store;
53
- try {
54
- store = read();
55
- }
56
- catch (error) {
57
- console.error(`could not read push subscriptions: ${error}`);
58
- return;
59
- }
60
- const subscriptions = store.subscriptions.filter(ours);
61
- if (subscriptions.length === 0)
62
- return;
63
- const payload = JSON.stringify({
64
- ...message,
65
- title: [...message.title].slice(0, TITLE_LIMIT).join(''),
66
- body: [...message.body].slice(0, BODY_LIMIT).join(''),
67
- });
68
- const gone = [];
69
- await Promise.all(subscriptions.map(async (subscription) => {
70
- try {
71
- await webPush.sendNotification(subscription, payload, {
72
- vapidDetails: {
73
- subject: SUBJECT,
74
- publicKey: store.publicKey,
75
- privateKey: store.privateKey,
76
- },
77
- TTL: TIME_TO_LIVE,
78
- urgency: 'high',
79
- timeout: SEND_TIMEOUT,
80
- });
81
- }
82
- catch (error) {
83
- const status = error.statusCode;
84
- if (status !== undefined && [401, 403, 404, 410].includes(status)) {
85
- console.log(`push subscription ${subscription.endpoint} is gone or was made for another key, removing it`);
86
- gone.push(subscription.endpoint);
87
- return;
88
- }
89
- console.warn(`could not send a push to ${subscription.endpoint}: ${error}`);
90
- }
91
- }));
92
- if (gone.length === 0)
93
- return;
94
- try {
95
- update((current) => {
96
- current.subscriptions = current.subscriptions.filter((subscription) => !gone.includes(subscription.endpoint));
97
- });
98
- }
99
- catch (error) {
100
- console.error(`could not remove gone push subscriptions: ${error}`);
101
- }
102
- }
103
- function ours(subscription) {
104
- return subscription.server === server;
105
- }
106
- function track(store) {
107
- active = store.subscriptions.some(ours);
108
- }
109
- function load() {
110
- let store;
111
- try {
112
- store = readJson('push.json');
113
- }
114
- catch (error) {
115
- if (!(error instanceof SyntaxError))
116
- throw error;
117
- return replaceCorrupt(error);
118
- }
119
- if (store === null)
120
- return create();
121
- if (typeof store.publicKey !== 'string' ||
122
- typeof store.privateKey !== 'string' ||
123
- !Array.isArray(store.subscriptions))
124
- return replaceCorrupt(new Error('it is missing its keys or subscriptions'));
125
- return store;
126
- }
127
- function replaceCorrupt(error) {
128
- const aside = `${pushFile}.corrupt-${randomUUID()}`;
129
- renameSync(pushFile, aside);
130
- console.warn(`${pushFile} is corrupt, moved it to ${aside} and created a new one: ${error}`);
131
- return create();
132
- }
133
- function create() {
134
- const keys = webPush.generateVAPIDKeys();
135
- const store = { ...keys, subscriptions: [] };
136
- save(store);
137
- return store;
138
- }
139
- function save(store) {
140
- writeJson('push.json', store);
141
- }
142
- function read() {
143
- const store = load();
144
- track(store);
145
- return store;
146
- }
147
- function update(change) {
148
- const store = load();
149
- change(store);
150
- save(store);
151
- track(store);
152
- }
@@ -1,375 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { query, } from '@anthropic-ai/claude-agent-sdk';
3
- import { claudeOptions, recordLimit } from './claude.js';
4
- import { toContext, toMessage } from './content.js';
5
- import { broadcast, publish } from './hub.js';
6
- const IDLE_LIMIT = 10 * 60 * 1000;
7
- const imageTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
8
- export const runs = new Map();
9
- export function activity() {
10
- const result = {};
11
- for (const run of runs.values())
12
- result[run.sessionId] = {
13
- running: run.running,
14
- waiting: run.pending.size > 0,
15
- };
16
- return result;
17
- }
18
- export function broadcastActivity() {
19
- broadcast({ type: 'activity', activity: activity() });
20
- }
21
- export class Run {
22
- sessionId;
23
- messages = [];
24
- pending = new Map();
25
- query;
26
- closed;
27
- resumeAt;
28
- queue = [];
29
- settings;
30
- context = null;
31
- running = false;
32
- inbox = [];
33
- wake = null;
34
- idleTimer;
35
- stopping = false;
36
- constructor(options) {
37
- this.sessionId = options.sessionId;
38
- this.resumeAt = options.resumeAt;
39
- this.settings = options.settings;
40
- const settings = options.settings;
41
- this.query = query({
42
- prompt: this.input(),
43
- options: {
44
- ...claudeOptions(settings.accountId, options.cwd),
45
- ...(options.resume
46
- ? { resume: options.sessionId }
47
- : { sessionId: options.sessionId }),
48
- ...(options.resumeAt === null
49
- ? {}
50
- : { resumeSessionAt: options.resumeAt }),
51
- enableFileCheckpointing: true,
52
- ...(settings.permissionMode === null
53
- ? {}
54
- : { permissionMode: settings.permissionMode }),
55
- ...(settings.model === null ? {} : { model: settings.model }),
56
- ...(settings.effort === null ? {} : { effort: settings.effort }),
57
- allowDangerouslySkipPermissions: true,
58
- includePartialMessages: true,
59
- canUseTool: (toolName, input, context) => this.ask({
60
- toolName,
61
- input,
62
- title: context.title ?? null,
63
- suggestions: context.suggestions ?? [],
64
- signal: context.signal,
65
- }),
66
- },
67
- });
68
- runs.set(this.sessionId, this);
69
- this.closed = this.read();
70
- }
71
- submit(prompt, delivery) {
72
- if (!this.running) {
73
- this.push(prompt, undefined);
74
- return;
75
- }
76
- switch (delivery) {
77
- case 'steer':
78
- this.push(prompt, 'next');
79
- return;
80
- case 'queue':
81
- this.queue = [...this.queue, { id: randomUUID(), prompt }];
82
- this.publishQueue();
83
- }
84
- }
85
- unqueue(id) {
86
- this.queue = this.queue.filter((item) => item.id !== id);
87
- this.publishQueue();
88
- }
89
- async configure(settings) {
90
- if (settings.model !== this.settings.model)
91
- await this.query.setModel(settings.model ?? undefined);
92
- if (settings.effort !== this.settings.effort)
93
- await this.query.applyFlagSettings({ effortLevel: settings.effort });
94
- if (settings.permissionMode !== null &&
95
- settings.permissionMode !== this.settings.permissionMode)
96
- await this.query.setPermissionMode(settings.permissionMode);
97
- this.updateSettings({
98
- ...settings,
99
- accountId: this.settings.accountId,
100
- permissionMode: settings.permissionMode ?? this.settings.permissionMode,
101
- });
102
- }
103
- respond(requestId, decision, answers) {
104
- const pending = this.pending.get(requestId);
105
- if (pending === undefined)
106
- return;
107
- const input = pending.request.input;
108
- switch (decision) {
109
- case 'allow':
110
- this.settle(requestId, {
111
- behavior: 'allow',
112
- updatedInput: answers === null ? input : { ...input, answers },
113
- });
114
- return;
115
- case 'always':
116
- this.settle(requestId, {
117
- behavior: 'allow',
118
- updatedInput: input,
119
- updatedPermissions: pending.suggestions,
120
- });
121
- return;
122
- case 'deny':
123
- this.settle(requestId, {
124
- behavior: 'deny',
125
- message: 'The user rejected this action',
126
- interrupt: true,
127
- });
128
- }
129
- }
130
- stop() {
131
- this.stopping = true;
132
- runs.delete(this.sessionId);
133
- this.query.close();
134
- }
135
- push(prompt, priority) {
136
- clearTimeout(this.idleTimer);
137
- for (const image of prompt.images)
138
- if (!imageTypes.includes(image.mediaType))
139
- throw new Error(`${image.mediaType} images are not supported`);
140
- const uuid = randomUUID();
141
- this.inbox.push({
142
- type: 'user',
143
- message: {
144
- role: 'user',
145
- content: [
146
- ...prompt.images.map((image) => ({
147
- type: 'image',
148
- source: {
149
- type: 'base64',
150
- media_type: image.mediaType,
151
- data: image.data,
152
- },
153
- })),
154
- ...(prompt.text === ''
155
- ? []
156
- : [{ type: 'text', text: prompt.text }]),
157
- ],
158
- },
159
- parent_tool_use_id: null,
160
- uuid,
161
- ...(priority === undefined ? {} : { priority }),
162
- });
163
- this.wake?.();
164
- this.add({
165
- type: 'user',
166
- uuid,
167
- time: Date.now(),
168
- content: [
169
- ...prompt.images.map((image) => ({ type: 'image', ...image })),
170
- ...(prompt.text === ''
171
- ? []
172
- : [{ type: 'text', text: prompt.text }]),
173
- ],
174
- parent: null,
175
- });
176
- this.running = true;
177
- broadcastActivity();
178
- }
179
- async refreshContext() {
180
- this.context = toContext(await this.query.getContextUsage());
181
- publish(this.sessionId, {
182
- type: 'context',
183
- sessionId: this.sessionId,
184
- context: this.context,
185
- });
186
- }
187
- async *input() {
188
- while (true) {
189
- const next = this.inbox.shift();
190
- if (next !== undefined) {
191
- yield next;
192
- continue;
193
- }
194
- await new Promise((resolve) => {
195
- this.wake = resolve;
196
- });
197
- }
198
- }
199
- async read() {
200
- try {
201
- for await (const message of this.query)
202
- this.receive(message);
203
- }
204
- catch (error) {
205
- if (!this.stopping)
206
- this.add(notice(error instanceof Error ? error.message : String(error)));
207
- }
208
- clearTimeout(this.idleTimer);
209
- for (const id of this.pending.keys())
210
- this.settle(id, { behavior: 'deny', message: 'Session ended' });
211
- if (runs.get(this.sessionId) === this)
212
- runs.delete(this.sessionId);
213
- this.running = false;
214
- broadcastActivity();
215
- }
216
- receive(message) {
217
- switch (message.type) {
218
- case 'assistant':
219
- this.add(toMessage('assistant', message, Date.now()));
220
- if (message.error === 'rate_limit')
221
- this.add(notice('This account hit its usage limit. Pick another account below and send again.'));
222
- return;
223
- case 'user':
224
- if (message.isSynthetic)
225
- return;
226
- this.add(toMessage('user', { ...message, uuid: message.uuid ?? randomUUID() }, Date.now()));
227
- return;
228
- case 'stream_event':
229
- if (message.parent_tool_use_id === null &&
230
- message.event.type === 'content_block_delta' &&
231
- message.event.delta.type === 'text_delta')
232
- publish(this.sessionId, {
233
- type: 'delta',
234
- sessionId: this.sessionId,
235
- text: message.event.delta.text,
236
- });
237
- return;
238
- case 'rate_limit_event': {
239
- const accountId = this.settings.accountId;
240
- if (accountId === null)
241
- throw new Error('Rate limit event for a run without an account');
242
- broadcast({
243
- type: 'limits',
244
- accountId,
245
- limits: recordLimit(accountId, message.rate_limit_info),
246
- });
247
- return;
248
- }
249
- case 'result':
250
- if (message.subtype !== 'success')
251
- this.add(notice(message.errors.join('\n') || message.subtype));
252
- this.finishTurn();
253
- this.refreshContext().catch((error) => console.error(error));
254
- return;
255
- case 'system':
256
- switch (message.subtype) {
257
- case 'init':
258
- this.updateSettings({
259
- ...this.settings,
260
- permissionMode: message.permissionMode,
261
- });
262
- return;
263
- case 'status':
264
- if (message.permissionMode !== undefined)
265
- this.updateSettings({
266
- ...this.settings,
267
- permissionMode: message.permissionMode,
268
- });
269
- return;
270
- case 'compact_boundary':
271
- this.add({
272
- type: 'notice',
273
- uuid: message.uuid,
274
- time: Date.now(),
275
- text: 'Conversation compacted',
276
- isError: false,
277
- });
278
- return;
279
- case 'local_command_output':
280
- this.add({
281
- type: 'notice',
282
- uuid: message.uuid,
283
- time: Date.now(),
284
- text: message.content,
285
- isError: false,
286
- });
287
- }
288
- }
289
- }
290
- finishTurn() {
291
- const [next, ...rest] = this.queue;
292
- if (next !== undefined) {
293
- this.queue = rest;
294
- this.publishQueue();
295
- this.push(next.prompt, undefined);
296
- return;
297
- }
298
- this.running = false;
299
- broadcastActivity();
300
- this.idleTimer = setTimeout(() => {
301
- if (!this.running && this.pending.size === 0)
302
- this.stop();
303
- }, IDLE_LIMIT);
304
- }
305
- ask(options) {
306
- const request = {
307
- id: randomUUID(),
308
- toolName: options.toolName,
309
- input: options.input,
310
- title: options.title,
311
- canAlwaysAllow: options.suggestions.length > 0,
312
- };
313
- return new Promise((resolve) => {
314
- this.pending.set(request.id, {
315
- request,
316
- suggestions: options.suggestions,
317
- resolve,
318
- });
319
- publish(this.sessionId, {
320
- type: 'request',
321
- sessionId: this.sessionId,
322
- request,
323
- });
324
- broadcastActivity();
325
- options.signal.addEventListener('abort', () => this.settle(request.id, { behavior: 'deny', message: 'Cancelled' }));
326
- });
327
- }
328
- settle(requestId, result) {
329
- const pending = this.pending.get(requestId);
330
- if (pending === undefined)
331
- return;
332
- this.pending.delete(requestId);
333
- pending.resolve(result);
334
- publish(this.sessionId, {
335
- type: 'requestDone',
336
- sessionId: this.sessionId,
337
- requestId,
338
- });
339
- broadcastActivity();
340
- }
341
- add(message) {
342
- if (this.messages.some((item) => item.uuid === message.uuid))
343
- return;
344
- this.messages.push(message);
345
- publish(this.sessionId, {
346
- type: 'message',
347
- sessionId: this.sessionId,
348
- message,
349
- });
350
- }
351
- updateSettings(settings) {
352
- this.settings = settings;
353
- publish(this.sessionId, {
354
- type: 'settings',
355
- sessionId: this.sessionId,
356
- settings,
357
- });
358
- }
359
- publishQueue() {
360
- publish(this.sessionId, {
361
- type: 'queue',
362
- sessionId: this.sessionId,
363
- queue: this.queue,
364
- });
365
- }
366
- }
367
- function notice(text) {
368
- return {
369
- type: 'notice',
370
- uuid: randomUUID(),
371
- time: Date.now(),
372
- text,
373
- isError: true,
374
- };
375
- }
@@ -1,102 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { statSync } from 'node:fs';
3
- import { getSessionMessages } from '@anthropic-ai/claude-agent-sdk';
4
- import { history } from './history.js';
5
- import { publish } from './hub.js';
6
- import { Run, runs } from './run.js';
7
- export function stopAll() {
8
- for (const run of runs.values())
9
- run.stop();
10
- }
11
- export async function rewind(request) {
12
- const sessionId = request.sessionId;
13
- if (runs.get(sessionId)?.running === true)
14
- throw new Error('Stop Claude before rewinding');
15
- const run = runFor(sessionId, request.cwd, request.settings);
16
- let result = null;
17
- if (request.code) {
18
- const files = await run.query.rewindFiles(request.messageId, {
19
- dryRun: request.dryRun,
20
- });
21
- if (!files.canRewind)
22
- throw new Error(files.error ?? 'The files cannot be restored');
23
- result = {
24
- files: files.filesChanged ?? [],
25
- insertions: files.insertions ?? 0,
26
- deletions: files.deletions ?? 0,
27
- };
28
- }
29
- if (request.dryRun || !request.conversation)
30
- return result;
31
- const disk = await getSessionMessages(sessionId, { dir: request.cwd });
32
- const index = disk.findIndex((item) => item.uuid === request.messageId);
33
- if (index < 1)
34
- throw new Error('Cannot rewind the conversation to that point');
35
- run.stop();
36
- await run.closed;
37
- new Run({
38
- sessionId,
39
- cwd: request.cwd,
40
- resume: true,
41
- resumeAt: disk[index - 1].uuid,
42
- settings: request.settings,
43
- });
44
- publish(sessionId, await history(sessionId, request.cwd));
45
- return result;
46
- }
47
- export async function open(client, sessionId, cwd) {
48
- client.opening = sessionId;
49
- const event = await history(sessionId, cwd);
50
- if (client.opening !== sessionId)
51
- return;
52
- client.sessionId = sessionId;
53
- client.send(event);
54
- }
55
- export async function send(client, request) {
56
- if (!statSync(request.cwd, { throwIfNoEntry: false })?.isDirectory())
57
- throw new Error(`${request.cwd} is not a folder`);
58
- if (request.sessionId === null) {
59
- const sessionId = randomUUID();
60
- const run = new Run({
61
- sessionId,
62
- cwd: request.cwd,
63
- resume: false,
64
- resumeAt: null,
65
- settings: request.settings,
66
- });
67
- client.opening = sessionId;
68
- client.sessionId = sessionId;
69
- client.send({ type: 'started', sessionId, cwd: request.cwd });
70
- run.submit(request.prompt, request.delivery);
71
- return;
72
- }
73
- const existing = runs.get(request.sessionId);
74
- if (existing !== undefined &&
75
- !existing.running &&
76
- (await changedElsewhere(existing, request.cwd)))
77
- existing.stop();
78
- const run = runFor(request.sessionId, request.cwd, request.settings);
79
- if (run === existing)
80
- await run.configure(request.settings);
81
- else
82
- await open(client, request.sessionId, request.cwd);
83
- run.submit(request.prompt, request.delivery);
84
- }
85
- function runFor(sessionId, cwd, settings) {
86
- const run = runs.get(sessionId);
87
- if (run !== undefined && run.settings.accountId === settings.accountId)
88
- return run;
89
- if (run !== undefined) {
90
- if (run.running)
91
- throw new Error('Wait for this turn to finish before switching accounts');
92
- run.stop();
93
- }
94
- return new Run({ sessionId, cwd, resume: true, resumeAt: null, settings });
95
- }
96
- async function changedElsewhere(run, cwd) {
97
- if (run.messages.length === 0)
98
- return false;
99
- const history = await getSessionMessages(run.sessionId, { dir: cwd });
100
- const last = history.at(-1)?.uuid;
101
- return last !== undefined && !run.messages.some((item) => item.uuid === last);
102
- }