ftown-bridge 0.19.44 → 0.19.49
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/claude-mod/.claude-plugin/plugin.json +6 -0
- package/claude-mod/hooks/hooks.json +3 -0
- package/claude-mod/hooks/register.js +137 -0
- package/dist/agent-commands.js +17 -2
- package/dist/agent-commands.js.map +1 -1
- package/dist/attention-store.d.ts +21 -1
- package/dist/attention-store.js +131 -3
- package/dist/attention-store.js.map +1 -1
- package/dist/claude-mod-installer.d.ts +9 -0
- package/dist/claude-mod-installer.js +55 -0
- package/dist/claude-mod-installer.js.map +1 -0
- package/dist/claude-runner.d.ts +8 -0
- package/dist/claude-runner.js +11 -4
- package/dist/claude-runner.js.map +1 -1
- package/dist/ftown-cli.d.ts +1 -1
- package/dist/ftown-cli.js +11 -3
- package/dist/ftown-cli.js.map +1 -1
- package/dist/harness-registry.js +4 -0
- package/dist/harness-registry.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +719 -671
- package/dist/index.js.map +1 -1
- package/dist/install-ftown-command-cli.d.ts +1 -1
- package/dist/install-ftown-command-cli.js +35 -44
- package/dist/install-ftown-command-cli.js.map +1 -1
- package/dist/installation-manifest.d.ts +25 -0
- package/dist/installation-manifest.js +37 -0
- package/dist/installation-manifest.js.map +1 -0
- package/dist/local-api-server.d.ts +8 -0
- package/dist/local-api-server.js +73 -1
- package/dist/local-api-server.js.map +1 -1
- package/dist/mail-delivery.d.ts +30 -0
- package/dist/mail-delivery.js +202 -8
- package/dist/mail-delivery.js.map +1 -1
- package/dist/mcp/fleet.d.ts +2 -2
- package/dist/native-mail.d.ts +31 -0
- package/dist/native-mail.js +142 -0
- package/dist/native-mail.js.map +1 -0
- package/dist/production-dependencies.d.ts +13 -0
- package/dist/production-dependencies.js +175 -0
- package/dist/production-dependencies.js.map +1 -0
- package/dist/push-notifier.d.ts +40 -0
- package/dist/push-notifier.js +126 -0
- package/dist/push-notifier.js.map +1 -0
- package/dist/sessions-commands.js +278 -310
- package/dist/sessions-commands.js.map +1 -1
- package/dist/setup-state.d.ts +1 -2
- package/dist/setup-state.js +31 -11
- package/dist/setup-state.js.map +1 -1
- package/dist/setup.js +3 -1
- package/dist/setup.js.map +1 -1
- package/dist/start-commands.d.ts +10 -0
- package/dist/start-commands.js +15 -0
- package/dist/start-commands.js.map +1 -0
- package/opencode-plugin/ftown.js +142 -129
- package/package.json +3 -2
- package/skills/ftown/SKILL.md +7 -2
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Local API client for ftown cross-session control.
|
|
4
4
|
* Installed to ~/.ftown/ftown-sessions by ftown-bridge.
|
|
5
5
|
*/
|
|
6
|
+
import { Command, CommanderError } from 'commander';
|
|
6
7
|
import { api as request, loadPointer, listSessions, fetchInbox, sendMail } from './cli-client.js';
|
|
7
8
|
import { MAIL_TYPES, AUTOMATION_REMOVED_MESSAGE, formatMailMessage, } from './wire-types.js';
|
|
8
9
|
async function api(method, path, body, headers) {
|
|
@@ -32,29 +33,6 @@ async function resolveFanout(mode, self) {
|
|
|
32
33
|
.filter((s) => s.parentSessionId === parent && s.id !== self)
|
|
33
34
|
.map((s) => s.id);
|
|
34
35
|
}
|
|
35
|
-
function flag(args, name) {
|
|
36
|
-
const i = args.indexOf(name);
|
|
37
|
-
if (i === -1 || i + 1 >= args.length)
|
|
38
|
-
return undefined;
|
|
39
|
-
return args[i + 1];
|
|
40
|
-
}
|
|
41
|
-
function hasFlag(args, name) {
|
|
42
|
-
return args.includes(name);
|
|
43
|
-
}
|
|
44
|
-
/** Positional args, skipping flags and the values of value-taking flags. */
|
|
45
|
-
function positionals(args, valueFlags) {
|
|
46
|
-
const out = [];
|
|
47
|
-
for (let i = 0; i < args.length; i++) {
|
|
48
|
-
const a = args[i];
|
|
49
|
-
if (a.startsWith('--')) {
|
|
50
|
-
if (valueFlags.includes(a))
|
|
51
|
-
i++;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
out.push(a);
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
36
|
async function fetchUsageRow(id) {
|
|
59
37
|
const [{ data: usageData }, sessionData] = await Promise.all([
|
|
60
38
|
api('GET', `/api/sessions/${id}/usage`),
|
|
@@ -114,319 +92,309 @@ function formatUsageTable(rows) {
|
|
|
114
92
|
const render = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(' ').trimEnd();
|
|
115
93
|
return [render(header), ...body.map(render)].join('\n');
|
|
116
94
|
}
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
--parent-id <uuid> Explicit parent session id
|
|
161
|
-
--model <name> Model (cursor, codex)
|
|
162
|
-
--json Output raw JSON (default for most commands)
|
|
163
|
-
|
|
164
|
-
Screen/grep options:
|
|
165
|
-
--offset <n> Pagination offset (default: 0)
|
|
166
|
-
--limit <n> Max lines/matches (default: 200)
|
|
167
|
-
|
|
168
|
-
Grep options:
|
|
169
|
-
--pattern <regex> Required for grep
|
|
170
|
-
|
|
171
|
-
Reads ~/.ftown/bridge.json (ftown-bridge must be running).`);
|
|
95
|
+
function sessionProgram() {
|
|
96
|
+
const program = new Command().name('ftown sessions').description('List, create, read and drive ftown agent sessions');
|
|
97
|
+
program.exitOverride();
|
|
98
|
+
program.configureOutput({ writeOut: (text) => console.log(text.trimEnd()), writeErr: (text) => console.error(text.trimEnd()) });
|
|
99
|
+
const command = (name, description) => program.command(name).description(description)
|
|
100
|
+
.option('--json', 'Machine-readable JSON output (the default for most commands)')
|
|
101
|
+
.option('--plain', 'Human-readable output where supported');
|
|
102
|
+
const run = (name, args, cmd) => executeSessionCommand(name, args, cmd.opts());
|
|
103
|
+
command('list', 'List sessions').action((_opts, cmd) => run('list', [], cmd));
|
|
104
|
+
command('create', 'Spawn a new agent session')
|
|
105
|
+
.option('--shell <type>', 'Harness override; otherwise inherit the current session')
|
|
106
|
+
.option('--prompt <text>', 'Initial message').option('--workdir <path>', 'Working directory')
|
|
107
|
+
.option('--create-workdir', 'Create the working directory if missing')
|
|
108
|
+
.option('--name <label>', 'Dashboard name').option('--command <cmd>', 'Full command override')
|
|
109
|
+
.option('--parent', 'Link to the current session').option('--parent-id <uuid>', 'Explicit parent session')
|
|
110
|
+
.option('--model <name>', 'Model override')
|
|
111
|
+
.option('--orchestrator', 'Compatibility flag (no additional prompt or environment changes)')
|
|
112
|
+
.action((_opts, cmd) => run('create', [], cmd));
|
|
113
|
+
for (const name of ['get', 'running', 'remove', 'revive']) {
|
|
114
|
+
command(`${name} <session-id>`, { get: 'Session metadata', running: 'Check if session PTY is running', remove: 'Stop and remove a session', revive: 'Recreate an archived session' }[name])
|
|
115
|
+
.action((id, _opts, cmd) => run(name, [id], cmd));
|
|
116
|
+
}
|
|
117
|
+
command('screen <session-id>', 'Print terminal lines')
|
|
118
|
+
.option('--offset <n>', 'Pagination offset', '0').option('--limit <n>', 'Max lines', '200')
|
|
119
|
+
.action((id, _opts, cmd) => run('screen', [id], cmd));
|
|
120
|
+
command('grep <session-id>', 'Search terminal output')
|
|
121
|
+
.requiredOption('--pattern <regex>', 'Search expression').option('--offset <n>', 'Match offset', '0')
|
|
122
|
+
.option('--limit <n>', 'Max matches', '100')
|
|
123
|
+
.action((id, _opts, cmd) => run('grep', [id], cmd));
|
|
124
|
+
command('keys <session-id> <text...>', 'Send keys to a running session')
|
|
125
|
+
.action((id, text, _opts, cmd) => run('keys', [id, ...text], cmd));
|
|
126
|
+
command('tell [target] [message...]', 'Send mail to another session inbox')
|
|
127
|
+
.option('--parent', 'Message the current session parent').option('--children', 'Message its children')
|
|
128
|
+
.option('--siblings', 'Message its siblings').option('--keys', 'Inject terminal keystrokes instead of mail')
|
|
129
|
+
.option('--type <t>', `Mail type: ${MAIL_TYPES.join(' | ')}`).option('--thread <id>', 'Reply thread')
|
|
130
|
+
.action((target, text, _opts, cmd) => run('tell', [...(target === undefined ? [] : [target]), ...text], cmd));
|
|
131
|
+
command('inbox', 'Read own inbox (requires FTOWN_SESSION_ID)').alias('mail')
|
|
132
|
+
.option('--peek', 'Do not mark delivered').option('--limit <n>', 'Max messages').option('--all', 'Include delivered messages')
|
|
133
|
+
.action((_opts, cmd) => run('inbox', [], cmd));
|
|
134
|
+
command('usage <session-id...>', 'Model/token usage per session')
|
|
135
|
+
.action((ids, _opts, cmd) => run('usage', ids, cmd));
|
|
136
|
+
command('archive', 'List archived sessions').action((_opts, cmd) => run('archive', [], cmd));
|
|
137
|
+
return program;
|
|
172
138
|
}
|
|
173
139
|
export async function runSessions(argv) {
|
|
174
140
|
if (['loop', 'loops', 'loop-list'].includes(argv[0])) {
|
|
175
141
|
console.error(`ftown sessions: ${AUTOMATION_REMOVED_MESSAGE}`);
|
|
176
142
|
return 1;
|
|
177
143
|
}
|
|
178
|
-
|
|
179
|
-
usage();
|
|
180
|
-
return argv.length === 0 ? 1 : 0;
|
|
181
|
-
}
|
|
182
|
-
const cmd = argv[0];
|
|
183
|
-
const rest = argv.slice(1);
|
|
184
|
-
const jsonOut = !hasFlag(rest, '--plain');
|
|
144
|
+
const program = sessionProgram();
|
|
185
145
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
146
|
+
if (argv.length === 0) {
|
|
147
|
+
program.outputHelp({ error: true });
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
await program.parseAsync(argv, { from: 'user' });
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error instanceof CommanderError)
|
|
155
|
+
return error.exitCode;
|
|
156
|
+
console.error(`ftown sessions: ${error instanceof Error ? error.message : String(error)}`);
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function executeSessionCommand(cmd, rest, options) {
|
|
161
|
+
const jsonOut = !options.plain;
|
|
162
|
+
switch (cmd) {
|
|
163
|
+
case 'list': {
|
|
164
|
+
const { data } = await api('GET', '/api/sessions');
|
|
165
|
+
console.log(jsonOut ? JSON.stringify(data, null, 2) : formatSessionList(data));
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'create': {
|
|
169
|
+
const shellType = options.shell;
|
|
170
|
+
const prompt = options.prompt;
|
|
171
|
+
const workingDir = options.workdir;
|
|
172
|
+
const name = options.name;
|
|
173
|
+
const command = options.command;
|
|
174
|
+
const model = options.model;
|
|
175
|
+
const parentId = options.parentId;
|
|
176
|
+
const useParent = !!options.parent;
|
|
177
|
+
const createWorkdir = !!options.createWorkdir;
|
|
178
|
+
const body = {};
|
|
179
|
+
if (shellType)
|
|
180
|
+
body.shellType = shellType;
|
|
181
|
+
if (prompt)
|
|
182
|
+
body.prompt = prompt;
|
|
183
|
+
if (workingDir)
|
|
184
|
+
body.workingDir = workingDir;
|
|
185
|
+
if (name)
|
|
186
|
+
body.name = name;
|
|
187
|
+
if (command)
|
|
188
|
+
body.command = command;
|
|
189
|
+
if (model)
|
|
190
|
+
body.model = model;
|
|
191
|
+
if (parentId)
|
|
192
|
+
body.parentSessionId = parentId;
|
|
193
|
+
else if (useParent)
|
|
194
|
+
body.parentSessionId = true;
|
|
195
|
+
if (createWorkdir)
|
|
196
|
+
body.createMissingWorkingDir = true;
|
|
197
|
+
const caller = process.env.FTOWN_SESSION_ID?.trim();
|
|
198
|
+
const headers = caller ? { 'X-Ftown-Session-Id': caller } : undefined;
|
|
199
|
+
const { data } = await api('POST', '/api/sessions', body, headers);
|
|
200
|
+
console.log(jsonOut ? JSON.stringify(data, null, 2) : formatCreated(data));
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'get': {
|
|
204
|
+
const id = rest[0];
|
|
205
|
+
if (!id)
|
|
206
|
+
throw new Error('Missing session-id');
|
|
207
|
+
const { data } = await api('GET', `/api/sessions/${id}`);
|
|
208
|
+
console.log(JSON.stringify(data, null, 2));
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case 'screen': {
|
|
212
|
+
const id = rest[0];
|
|
213
|
+
if (!id)
|
|
214
|
+
throw new Error('Missing session-id');
|
|
215
|
+
const offset = options.offset ?? '0';
|
|
216
|
+
const limit = options.limit ?? '200';
|
|
217
|
+
const { data } = await api('GET', `/api/sessions/${id}/screen?offset=${offset}&limit=${limit}`);
|
|
218
|
+
if (jsonOut) {
|
|
232
219
|
console.log(JSON.stringify(data, null, 2));
|
|
233
|
-
break;
|
|
234
220
|
}
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const offset = flag(rest, '--offset') ?? '0';
|
|
240
|
-
const limit = flag(rest, '--limit') ?? '200';
|
|
241
|
-
const { data } = await api('GET', `/api/sessions/${id}/screen?offset=${offset}&limit=${limit}`);
|
|
242
|
-
if (jsonOut) {
|
|
243
|
-
console.log(JSON.stringify(data, null, 2));
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
const lines = data.lines ?? [];
|
|
247
|
-
for (const line of lines)
|
|
248
|
-
console.log(line);
|
|
249
|
-
}
|
|
250
|
-
break;
|
|
221
|
+
else {
|
|
222
|
+
const lines = data.lines ?? [];
|
|
223
|
+
for (const line of lines)
|
|
224
|
+
console.log(line);
|
|
251
225
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case 'grep': {
|
|
229
|
+
const id = rest[0];
|
|
230
|
+
const pattern = options.pattern;
|
|
231
|
+
if (!id)
|
|
232
|
+
throw new Error('Missing session-id');
|
|
233
|
+
if (!pattern)
|
|
234
|
+
throw new Error('Missing --pattern');
|
|
235
|
+
const offset = parseInt(options.offset ?? '0', 10);
|
|
236
|
+
const limit = parseInt(options.limit ?? '100', 10);
|
|
237
|
+
const { data } = await api('POST', `/api/sessions/${id}/grep`, {
|
|
238
|
+
pattern,
|
|
239
|
+
offset,
|
|
240
|
+
limit,
|
|
241
|
+
});
|
|
242
|
+
console.log(JSON.stringify(data, null, 2));
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case 'keys': {
|
|
246
|
+
const positional = rest;
|
|
247
|
+
const id = positional[0];
|
|
248
|
+
const keys = positional.slice(1).join(' ');
|
|
249
|
+
if (!id || !keys)
|
|
250
|
+
throw new Error('Usage: keys <session-id> <text>');
|
|
251
|
+
await api('POST', `/api/sessions/${id}/keys`, { keys });
|
|
252
|
+
console.log(JSON.stringify({ sent: true, sessionId: id }));
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case 'tell': {
|
|
256
|
+
const useParent = !!options.parent;
|
|
257
|
+
const useChildren = !!options.children;
|
|
258
|
+
const useSiblings = !!options.siblings;
|
|
259
|
+
const useKeys = !!options.keys;
|
|
260
|
+
const mailType = options.type;
|
|
261
|
+
const threadId = options.thread;
|
|
262
|
+
const fanCount = [useParent, useChildren, useSiblings].filter(Boolean).length;
|
|
263
|
+
if (fanCount > 1) {
|
|
264
|
+
throw new Error('Use only one of --parent, --children, --siblings');
|
|
268
265
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const id = positional[0];
|
|
272
|
-
const keys = positional.slice(1).join(' ');
|
|
273
|
-
if (!id || !keys)
|
|
274
|
-
throw new Error('Usage: keys <session-id> <text>');
|
|
275
|
-
await api('POST', `/api/sessions/${id}/keys`, { keys });
|
|
276
|
-
console.log(JSON.stringify({ sent: true, sessionId: id }));
|
|
277
|
-
break;
|
|
266
|
+
if (mailType && !MAIL_TYPES.includes(mailType)) {
|
|
267
|
+
throw new Error(`Invalid --type "${mailType}" — use one of: ${MAIL_TYPES.join(', ')}`);
|
|
278
268
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const threadId = flag(rest, '--thread');
|
|
286
|
-
const fanCount = [useParent, useChildren, useSiblings].filter(Boolean).length;
|
|
287
|
-
if (fanCount > 1) {
|
|
288
|
-
throw new Error('Use only one of --parent, --children, --siblings');
|
|
289
|
-
}
|
|
290
|
-
if (mailType && !MAIL_TYPES.includes(mailType)) {
|
|
291
|
-
throw new Error(`Invalid --type "${mailType}" — use one of: ${MAIL_TYPES.join(', ')}`);
|
|
292
|
-
}
|
|
293
|
-
const positional = positionals(rest, ['--type', '--thread']);
|
|
294
|
-
const self = process.env.FTOWN_SESSION_ID?.trim();
|
|
295
|
-
let targets;
|
|
296
|
-
let message;
|
|
297
|
-
if (fanCount === 1) {
|
|
298
|
-
message = positional.join(' ');
|
|
299
|
-
targets = await resolveFanout(useParent ? 'parent' : useChildren ? 'children' : 'siblings', self);
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
const target = positional[0];
|
|
303
|
-
message = positional.slice(1).join(' ');
|
|
304
|
-
if (!target) {
|
|
305
|
-
throw new Error('Usage: tell <target|--parent|--children|--siblings> <message...>');
|
|
306
|
-
}
|
|
307
|
-
targets = [target];
|
|
308
|
-
}
|
|
269
|
+
const positional = rest;
|
|
270
|
+
const self = process.env.FTOWN_SESSION_ID?.trim();
|
|
271
|
+
let targets;
|
|
272
|
+
let message;
|
|
273
|
+
if (fanCount === 1) {
|
|
274
|
+
message = positional.join(' ');
|
|
309
275
|
if (!message)
|
|
310
276
|
throw new Error('Missing message');
|
|
311
|
-
|
|
312
|
-
throw new Error('No matching target sessions');
|
|
313
|
-
const fromName = self
|
|
314
|
-
? (await listSessionInfo()).find((s) => s.id === self)?.name
|
|
315
|
-
: undefined;
|
|
316
|
-
for (const target of targets) {
|
|
317
|
-
try {
|
|
318
|
-
if (useKeys) {
|
|
319
|
-
// Explicit keystroke injection into the target terminal (legacy path).
|
|
320
|
-
const reqBody = { text: message };
|
|
321
|
-
if (self)
|
|
322
|
-
reqBody.from = self;
|
|
323
|
-
const { data } = await api('POST', `/api/sessions/${target}/message`, reqBody);
|
|
324
|
-
console.log(JSON.stringify({ target, ...data }));
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
const reqBody = { body: message };
|
|
328
|
-
if (self)
|
|
329
|
-
reqBody.from = self;
|
|
330
|
-
if (fromName)
|
|
331
|
-
reqBody.fromName = fromName;
|
|
332
|
-
if (mailType)
|
|
333
|
-
reqBody.type = mailType;
|
|
334
|
-
if (threadId)
|
|
335
|
-
reqBody.threadId = threadId;
|
|
336
|
-
const data = await sendMail(loadPointer(), target, reqBody);
|
|
337
|
-
console.log(JSON.stringify({ target, ...data }));
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
catch (err) {
|
|
341
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
342
|
-
console.log(JSON.stringify({ target, error: msg }));
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
break;
|
|
277
|
+
targets = await resolveFanout(useParent ? 'parent' : useChildren ? 'children' : 'siblings', self);
|
|
346
278
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (!
|
|
351
|
-
throw new Error('
|
|
352
|
-
const limit = flag(rest, '--limit');
|
|
353
|
-
const messages = await fetchInbox(loadPointer(), self, { peek: hasFlag(rest, '--peek'), all: hasFlag(rest, '--all'), ...(limit ? { limit: Number(limit) } : {}) });
|
|
354
|
-
if (hasFlag(rest, '--json')) {
|
|
355
|
-
console.log(JSON.stringify({ messages }, null, 2));
|
|
279
|
+
else {
|
|
280
|
+
const target = positional[0];
|
|
281
|
+
message = positional.slice(1).join(' ');
|
|
282
|
+
if (!target) {
|
|
283
|
+
throw new Error('Usage: tell <target|--parent|--children|--siblings> <message...>');
|
|
356
284
|
}
|
|
357
|
-
|
|
358
|
-
|
|
285
|
+
targets = [target];
|
|
286
|
+
}
|
|
287
|
+
if (!message)
|
|
288
|
+
throw new Error('Missing message');
|
|
289
|
+
if (targets.length === 0)
|
|
290
|
+
throw new Error('No matching target sessions');
|
|
291
|
+
const fromName = self
|
|
292
|
+
? (await listSessionInfo()).find((s) => s.id === self)?.name
|
|
293
|
+
: undefined;
|
|
294
|
+
for (const target of targets) {
|
|
295
|
+
try {
|
|
296
|
+
if (useKeys) {
|
|
297
|
+
// Explicit keystroke injection into the target terminal (legacy path).
|
|
298
|
+
const reqBody = { text: message };
|
|
299
|
+
if (self)
|
|
300
|
+
reqBody.from = self;
|
|
301
|
+
const { data } = await api('POST', `/api/sessions/${target}/message`, reqBody);
|
|
302
|
+
console.log(JSON.stringify({ target, ...data }));
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
const reqBody = { body: message };
|
|
306
|
+
if (self)
|
|
307
|
+
reqBody.from = self;
|
|
308
|
+
if (fromName)
|
|
309
|
+
reqBody.fromName = fromName;
|
|
310
|
+
if (mailType)
|
|
311
|
+
reqBody.type = mailType;
|
|
312
|
+
if (threadId)
|
|
313
|
+
reqBody.threadId = threadId;
|
|
314
|
+
const data = await sendMail(loadPointer(), target, reqBody);
|
|
315
|
+
console.log(JSON.stringify({ target, ...data }));
|
|
316
|
+
}
|
|
359
317
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
318
|
+
catch (err) {
|
|
319
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
320
|
+
console.log(JSON.stringify({ target, error: msg }));
|
|
363
321
|
}
|
|
364
|
-
break;
|
|
365
322
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
case 'inbox':
|
|
326
|
+
case 'mail': {
|
|
327
|
+
const self = process.env.FTOWN_SESSION_ID?.trim();
|
|
328
|
+
if (!self)
|
|
329
|
+
throw new Error('inbox requires FTOWN_SESSION_ID to be set');
|
|
330
|
+
const limit = options.limit;
|
|
331
|
+
const messages = await fetchInbox(loadPointer(), self, { peek: !!options.peek, all: !!options.all, ...(limit ? { limit: Number(limit) } : {}) });
|
|
332
|
+
if (!!options.json) {
|
|
333
|
+
console.log(JSON.stringify({ messages }, null, 2));
|
|
373
334
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (ids.length === 0)
|
|
377
|
-
throw new Error('Missing session-id');
|
|
378
|
-
const rows = await Promise.all(ids.map(fetchUsageRow));
|
|
379
|
-
if (hasFlag(rest, '--json')) {
|
|
380
|
-
console.log(JSON.stringify({ sessions: rows }, null, 2));
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
console.log(formatUsageTable(rows));
|
|
384
|
-
}
|
|
385
|
-
break;
|
|
335
|
+
else if (messages.length === 0) {
|
|
336
|
+
console.log('(no mail)');
|
|
386
337
|
}
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
throw new Error('Missing session-id');
|
|
391
|
-
const { data } = await api('DELETE', `/api/sessions/${id}`);
|
|
392
|
-
console.log(jsonOut ? JSON.stringify(data, null, 2) : `removed ${id}`);
|
|
393
|
-
break;
|
|
338
|
+
else {
|
|
339
|
+
for (const m of messages)
|
|
340
|
+
console.log(formatMailMessage(m));
|
|
394
341
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case 'running': {
|
|
345
|
+
const id = rest[0];
|
|
346
|
+
if (!id)
|
|
347
|
+
throw new Error('Missing session-id');
|
|
348
|
+
const { data } = await api('GET', `/api/sessions/${id}/running`);
|
|
349
|
+
console.log(JSON.stringify(data, null, 2));
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case 'usage': {
|
|
353
|
+
const ids = rest;
|
|
354
|
+
if (ids.length === 0)
|
|
355
|
+
throw new Error('Missing session-id');
|
|
356
|
+
const rows = await Promise.all(ids.map(fetchUsageRow));
|
|
357
|
+
if (!!options.json) {
|
|
358
|
+
console.log(JSON.stringify({ sessions: rows }, null, 2));
|
|
410
359
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (!id)
|
|
414
|
-
throw new Error('Missing session-id');
|
|
415
|
-
const { data } = await api('POST', `/api/sessions/${id}/revive`);
|
|
416
|
-
console.log(jsonOut ? JSON.stringify(data, null, 2) : formatCreated(data));
|
|
417
|
-
break;
|
|
360
|
+
else {
|
|
361
|
+
console.log(formatUsageTable(rows));
|
|
418
362
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case 'remove': {
|
|
366
|
+
const id = rest[0];
|
|
367
|
+
if (!id)
|
|
368
|
+
throw new Error('Missing session-id');
|
|
369
|
+
const { data } = await api('DELETE', `/api/sessions/${id}`);
|
|
370
|
+
console.log(jsonOut ? JSON.stringify(data, null, 2) : `removed ${id}`);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case 'archive': {
|
|
374
|
+
const { data } = await api('GET', '/api/archive');
|
|
375
|
+
const archived = data.archived ?? [];
|
|
376
|
+
const summary = archived.map((s) => ({
|
|
377
|
+
id: s.id,
|
|
378
|
+
name: s.name,
|
|
379
|
+
removedAt: s.removedAt,
|
|
380
|
+
shellType: s.shellType,
|
|
381
|
+
}));
|
|
382
|
+
console.log(jsonOut
|
|
383
|
+
? JSON.stringify({ archived: summary }, null, 2)
|
|
384
|
+
: summary
|
|
385
|
+
.map((s) => `${s.id} ${s.removedAt} ${s.shellType ?? ''} ${s.name ?? ''}`.trimEnd())
|
|
386
|
+
.join('\n'));
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
case 'revive': {
|
|
390
|
+
const id = rest[0];
|
|
391
|
+
if (!id)
|
|
392
|
+
throw new Error('Missing session-id');
|
|
393
|
+
const { data } = await api('POST', `/api/sessions/${id}/revive`);
|
|
394
|
+
console.log(jsonOut ? JSON.stringify(data, null, 2) : formatCreated(data));
|
|
395
|
+
break;
|
|
422
396
|
}
|
|
423
397
|
}
|
|
424
|
-
catch (err) {
|
|
425
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
426
|
-
console.error(`ftown sessions: ${msg}`);
|
|
427
|
-
return 1;
|
|
428
|
-
}
|
|
429
|
-
return 0;
|
|
430
398
|
}
|
|
431
399
|
function formatSessionList(data) {
|
|
432
400
|
const sessions = data.sessions ?? [];
|