lunel-cli 0.1.117 → 0.1.119
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/codex.d.ts +2 -1
- package/dist/ai/codex.js +58 -0
- package/dist/ai/index.d.ts +18 -0
- package/dist/ai/index.js +45 -0
- package/dist/ai/interface.d.ts +14 -0
- package/dist/ai/opencode.d.ts +5 -1
- package/dist/ai/opencode.js +83 -0
- package/dist/index.js +282 -4
- package/package.json +1 -1
package/dist/ai/codex.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AIProvider, AiEventEmitter, CodexPromptOptions, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
|
|
1
|
+
import type { AIProvider, AiEventEmitter, CodexPromptOptions, AiSyncState, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
|
|
2
2
|
export declare class CodexProvider implements AIProvider {
|
|
3
3
|
private proc;
|
|
4
4
|
private shuttingDown;
|
|
@@ -36,6 +36,7 @@ export declare class CodexProvider implements AIProvider {
|
|
|
36
36
|
getMessages(sessionId: string): Promise<{
|
|
37
37
|
messages: MessageInfo[];
|
|
38
38
|
}>;
|
|
39
|
+
syncState(sessionIds?: string[]): Promise<AiSyncState>;
|
|
39
40
|
prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
|
|
40
41
|
ack: true;
|
|
41
42
|
}>;
|
package/dist/ai/codex.js
CHANGED
|
@@ -268,6 +268,64 @@ export class CodexProvider {
|
|
|
268
268
|
}
|
|
269
269
|
return { messages: session.messages };
|
|
270
270
|
}
|
|
271
|
+
async syncState(sessionIds = []) {
|
|
272
|
+
await this.listSessions().catch(() => ({ sessions: [] }));
|
|
273
|
+
const messageSessionIds = new Set(sessionIds);
|
|
274
|
+
for (const session of this.sessions.values()) {
|
|
275
|
+
if (session.activeTurnId)
|
|
276
|
+
messageSessionIds.add(session.id);
|
|
277
|
+
}
|
|
278
|
+
for (const pending of this.pendingPermissionRequestIds.values()) {
|
|
279
|
+
if (pending.sessionId)
|
|
280
|
+
messageSessionIds.add(pending.sessionId);
|
|
281
|
+
}
|
|
282
|
+
for (const pending of this.pendingQuestionRequestIds.values()) {
|
|
283
|
+
if (pending.sessionId)
|
|
284
|
+
messageSessionIds.add(pending.sessionId);
|
|
285
|
+
}
|
|
286
|
+
const messages = {};
|
|
287
|
+
const statuses = [];
|
|
288
|
+
await Promise.allSettled(Array.from(messageSessionIds).map(async (sessionId) => {
|
|
289
|
+
const response = await this.getMessages(sessionId);
|
|
290
|
+
messages[sessionId] = response.messages;
|
|
291
|
+
const session = this.sessions.get(sessionId);
|
|
292
|
+
if (!session)
|
|
293
|
+
return;
|
|
294
|
+
const activeTurnId = await this.resolveInFlightTurnId(sessionId).catch(() => undefined);
|
|
295
|
+
session.activeTurnId = activeTurnId;
|
|
296
|
+
if (activeTurnId) {
|
|
297
|
+
statuses.push({
|
|
298
|
+
sessionID: sessionId,
|
|
299
|
+
status: { type: "running", activeTurnId },
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}));
|
|
303
|
+
return {
|
|
304
|
+
sessions: Array.from(this.sessions.values()).map((session) => this.toSessionInfo(session)),
|
|
305
|
+
statuses,
|
|
306
|
+
messages,
|
|
307
|
+
pendingPermissions: Array.from(this.pendingPermissionRequestIds.entries()).map(([id, pending]) => ({
|
|
308
|
+
id,
|
|
309
|
+
sessionID: pending.sessionId,
|
|
310
|
+
messageID: pending.messageId,
|
|
311
|
+
callID: pending.callId,
|
|
312
|
+
type: pending.method,
|
|
313
|
+
title: pending.method,
|
|
314
|
+
metadata: { method: pending.method },
|
|
315
|
+
})),
|
|
316
|
+
pendingQuestions: Array.from(this.pendingQuestionRequestIds.entries()).map(([id, pending]) => ({
|
|
317
|
+
id,
|
|
318
|
+
sessionID: pending.sessionId,
|
|
319
|
+
questions: [],
|
|
320
|
+
tool: {
|
|
321
|
+
...(pending.messageId ? { messageID: pending.messageId } : {}),
|
|
322
|
+
...(pending.callId ? { callID: pending.callId } : {}),
|
|
323
|
+
},
|
|
324
|
+
})),
|
|
325
|
+
statusAuthoritative: true,
|
|
326
|
+
generatedAt: Date.now(),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
271
329
|
async prompt(sessionId, text, model, agent, files = [], codexOptions) {
|
|
272
330
|
const session = this.ensureLocalSession(sessionId);
|
|
273
331
|
session.updatedAt = Date.now();
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -14,6 +14,24 @@ export declare class AiManager {
|
|
|
14
14
|
backend: AiBackend;
|
|
15
15
|
}>;
|
|
16
16
|
}>;
|
|
17
|
+
syncState(sessionIds?: Partial<Record<AiBackend, string[]>>): Promise<{
|
|
18
|
+
sessions: Array<Record<string, unknown> & {
|
|
19
|
+
backend: AiBackend;
|
|
20
|
+
}>;
|
|
21
|
+
statuses: Array<Record<string, unknown> & {
|
|
22
|
+
backend: AiBackend;
|
|
23
|
+
}>;
|
|
24
|
+
messages: Record<string, unknown>;
|
|
25
|
+
pendingPermissions: Array<Record<string, unknown> & {
|
|
26
|
+
backend: AiBackend;
|
|
27
|
+
}>;
|
|
28
|
+
pendingQuestions: Array<Record<string, unknown> & {
|
|
29
|
+
backend: AiBackend;
|
|
30
|
+
}>;
|
|
31
|
+
statusAuthoritativeByBackend: Partial<Record<AiBackend, boolean>>;
|
|
32
|
+
syncedBackends: AiBackend[];
|
|
33
|
+
generatedAt: number;
|
|
34
|
+
}>;
|
|
17
35
|
createSession(backend: AiBackend, title?: string): Promise<{
|
|
18
36
|
session: import("./interface.js").SessionInfo;
|
|
19
37
|
}>;
|
package/dist/ai/index.js
CHANGED
|
@@ -68,6 +68,51 @@ export class AiManager {
|
|
|
68
68
|
const sessions = results.flatMap((r) => (r.status === "fulfilled" ? r.value : []));
|
|
69
69
|
return { sessions };
|
|
70
70
|
}
|
|
71
|
+
async syncState(sessionIds = {}) {
|
|
72
|
+
const results = await Promise.allSettled(this._available.map(async (backend) => {
|
|
73
|
+
const provider = this._providers[backend];
|
|
74
|
+
const state = provider.syncState
|
|
75
|
+
? await provider.syncState(sessionIds[backend])
|
|
76
|
+
: {
|
|
77
|
+
sessions: (await provider.listSessions()).sessions,
|
|
78
|
+
statuses: [],
|
|
79
|
+
messages: {},
|
|
80
|
+
generatedAt: Date.now(),
|
|
81
|
+
};
|
|
82
|
+
return { backend, state };
|
|
83
|
+
}));
|
|
84
|
+
const sessions = [];
|
|
85
|
+
const statuses = [];
|
|
86
|
+
const messages = {};
|
|
87
|
+
const pendingPermissions = [];
|
|
88
|
+
const pendingQuestions = [];
|
|
89
|
+
const statusAuthoritativeByBackend = {};
|
|
90
|
+
const syncedBackends = [];
|
|
91
|
+
for (const result of results) {
|
|
92
|
+
if (result.status !== "fulfilled")
|
|
93
|
+
continue;
|
|
94
|
+
const { backend, state } = result.value;
|
|
95
|
+
syncedBackends.push(backend);
|
|
96
|
+
statusAuthoritativeByBackend[backend] = state.statusAuthoritative !== false;
|
|
97
|
+
sessions.push(...(state.sessions ?? []).map((session) => ({ ...session, backend })));
|
|
98
|
+
statuses.push(...(state.statuses ?? []).map((status) => ({ ...status, backend })));
|
|
99
|
+
for (const [sessionId, value] of Object.entries(state.messages ?? {})) {
|
|
100
|
+
messages[`${backend}:${sessionId}`] = value;
|
|
101
|
+
}
|
|
102
|
+
pendingPermissions.push(...(state.pendingPermissions ?? []).map((permission) => ({ ...permission, backend })));
|
|
103
|
+
pendingQuestions.push(...(state.pendingQuestions ?? []).map((question) => ({ ...question, backend })));
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
sessions,
|
|
107
|
+
statuses,
|
|
108
|
+
messages,
|
|
109
|
+
pendingPermissions,
|
|
110
|
+
pendingQuestions,
|
|
111
|
+
statusAuthoritativeByBackend,
|
|
112
|
+
syncedBackends,
|
|
113
|
+
generatedAt: Date.now(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
71
116
|
// Session management — all require explicit backend
|
|
72
117
|
createSession(backend, title) { return this.get(backend).createSession(title); }
|
|
73
118
|
getSession(backend, id) { return this.get(backend).getSession(id); }
|
package/dist/ai/interface.d.ts
CHANGED
|
@@ -35,6 +35,19 @@ export interface ProviderInfo {
|
|
|
35
35
|
default: Record<string, string>;
|
|
36
36
|
[key: string]: unknown;
|
|
37
37
|
}
|
|
38
|
+
export interface AiSessionStatus {
|
|
39
|
+
sessionID: string;
|
|
40
|
+
status: Record<string, unknown> | string;
|
|
41
|
+
}
|
|
42
|
+
export interface AiSyncState {
|
|
43
|
+
sessions: unknown[];
|
|
44
|
+
statuses: AiSessionStatus[];
|
|
45
|
+
messages: Record<string, MessageInfo[]>;
|
|
46
|
+
pendingPermissions?: Record<string, unknown>[];
|
|
47
|
+
pendingQuestions?: Record<string, unknown>[];
|
|
48
|
+
statusAuthoritative?: boolean;
|
|
49
|
+
generatedAt: number;
|
|
50
|
+
}
|
|
38
51
|
/**
|
|
39
52
|
* Every AI backend (OpenCode, Codex, …) implements this interface.
|
|
40
53
|
* Method names map 1-to-1 with the "ai" namespace actions in index.ts.
|
|
@@ -67,6 +80,7 @@ export interface AIProvider {
|
|
|
67
80
|
getMessages(sessionId: string): Promise<{
|
|
68
81
|
messages: MessageInfo[];
|
|
69
82
|
}>;
|
|
83
|
+
syncState?(sessionIds?: string[]): Promise<AiSyncState>;
|
|
70
84
|
prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
|
|
71
85
|
ack: true;
|
|
72
86
|
}>;
|
package/dist/ai/opencode.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AIProvider, AiEventEmitter, CodexPromptOptions, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
|
|
1
|
+
import type { AIProvider, AiEventEmitter, CodexPromptOptions, AiSyncState, FileAttachment, ModelSelector, MessageInfo, ProviderInfo, SessionInfo, ShareInfo } from "./interface.js";
|
|
2
2
|
export declare class OpenCodeProvider implements AIProvider {
|
|
3
3
|
private client;
|
|
4
4
|
private server;
|
|
@@ -33,6 +33,7 @@ export declare class OpenCodeProvider implements AIProvider {
|
|
|
33
33
|
getMessages(sessionId: string): Promise<{
|
|
34
34
|
messages: MessageInfo[];
|
|
35
35
|
}>;
|
|
36
|
+
syncState(sessionIds?: string[]): Promise<AiSyncState>;
|
|
36
37
|
prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
|
|
37
38
|
ack: true;
|
|
38
39
|
}>;
|
|
@@ -57,6 +58,9 @@ export declare class OpenCodeProvider implements AIProvider {
|
|
|
57
58
|
private sendPromptAsync;
|
|
58
59
|
private reconcileOpenCodeState;
|
|
59
60
|
private refreshBusySessionMessages;
|
|
61
|
+
private fetchSessionStatuses;
|
|
62
|
+
private listPendingPermissions;
|
|
63
|
+
private listPendingQuestions;
|
|
60
64
|
private refreshSessionsMetadata;
|
|
61
65
|
private refreshPendingPermissions;
|
|
62
66
|
private refreshPendingQuestions;
|
package/dist/ai/opencode.js
CHANGED
|
@@ -393,6 +393,52 @@ export class OpenCodeProvider {
|
|
|
393
393
|
throw err;
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
|
+
async syncState(sessionIds = []) {
|
|
397
|
+
const [sessionsResult, statusesResult, permissionsResult, questionsResult] = await Promise.allSettled([
|
|
398
|
+
this.listSessions(),
|
|
399
|
+
this.fetchSessionStatuses(),
|
|
400
|
+
this.listPendingPermissions(),
|
|
401
|
+
this.listPendingQuestions(),
|
|
402
|
+
]);
|
|
403
|
+
const sessions = sessionsResult.status === "fulfilled"
|
|
404
|
+
? (sessionsResult.value.sessions ?? [])
|
|
405
|
+
: [];
|
|
406
|
+
const statuses = statusesResult.status === "fulfilled" ? statusesResult.value : [];
|
|
407
|
+
const pendingPermissions = permissionsResult.status === "fulfilled" ? permissionsResult.value : [];
|
|
408
|
+
const pendingQuestions = questionsResult.status === "fulfilled" ? questionsResult.value : [];
|
|
409
|
+
const messageSessionIds = new Set(sessionIds);
|
|
410
|
+
for (const entry of statuses) {
|
|
411
|
+
const statusObj = typeof entry.status === "object" && entry.status !== null ? entry.status : {};
|
|
412
|
+
const statusType = typeof statusObj.type === "string" ? statusObj.type.toLowerCase() : String(entry.status ?? "").toLowerCase();
|
|
413
|
+
if (statusType === "busy" || statusType === "running" || statusType === "working" || statusType === "retry") {
|
|
414
|
+
messageSessionIds.add(entry.sessionID);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
for (const permission of pendingPermissions) {
|
|
418
|
+
const sessionID = this.readString(permission.sessionID) ?? this.readString(permission.sessionId);
|
|
419
|
+
if (sessionID)
|
|
420
|
+
messageSessionIds.add(sessionID);
|
|
421
|
+
}
|
|
422
|
+
for (const question of pendingQuestions) {
|
|
423
|
+
const sessionID = this.readString(question.sessionID) ?? this.readString(question.sessionId);
|
|
424
|
+
if (sessionID)
|
|
425
|
+
messageSessionIds.add(sessionID);
|
|
426
|
+
}
|
|
427
|
+
const messages = {};
|
|
428
|
+
await Promise.allSettled(Array.from(messageSessionIds).map(async (sessionId) => {
|
|
429
|
+
const response = await this.getMessages(sessionId);
|
|
430
|
+
messages[sessionId] = response.messages;
|
|
431
|
+
}));
|
|
432
|
+
return {
|
|
433
|
+
sessions,
|
|
434
|
+
statuses,
|
|
435
|
+
messages,
|
|
436
|
+
pendingPermissions,
|
|
437
|
+
pendingQuestions,
|
|
438
|
+
statusAuthoritative: statusesResult.status === "fulfilled",
|
|
439
|
+
generatedAt: Date.now(),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
396
442
|
// -------------------------------------------------------------------------
|
|
397
443
|
// Interaction
|
|
398
444
|
// -------------------------------------------------------------------------
|
|
@@ -690,6 +736,43 @@ export class OpenCodeProvider {
|
|
|
690
736
|
}
|
|
691
737
|
}
|
|
692
738
|
}
|
|
739
|
+
async fetchSessionStatuses() {
|
|
740
|
+
const payload = await this.fetchOpenCodeJson("/session/status", { method: "GET" });
|
|
741
|
+
if (!payload || typeof payload !== "object")
|
|
742
|
+
return [];
|
|
743
|
+
return Object.entries(payload).map(([sessionID, status]) => ({
|
|
744
|
+
sessionID,
|
|
745
|
+
status: status,
|
|
746
|
+
}));
|
|
747
|
+
}
|
|
748
|
+
async listPendingPermissions() {
|
|
749
|
+
const permissionApi = this.client?.permission;
|
|
750
|
+
if (!permissionApi?.list)
|
|
751
|
+
return [];
|
|
752
|
+
const response = await permissionApi.list();
|
|
753
|
+
const data = Array.isArray(response.data) ? response.data : [];
|
|
754
|
+
return data
|
|
755
|
+
.map((entry) => normalizePermissionProperties(this.asRecord(entry)))
|
|
756
|
+
.filter((entry) => !!this.readString(entry.id));
|
|
757
|
+
}
|
|
758
|
+
async listPendingQuestions() {
|
|
759
|
+
const data = await this.fetchOpenCodeJson("/question", { method: "GET" });
|
|
760
|
+
const questions = Array.isArray(data) ? data : [];
|
|
761
|
+
return questions
|
|
762
|
+
.flatMap((entry) => {
|
|
763
|
+
const question = this.asRecord(entry);
|
|
764
|
+
const id = this.readString(question.id);
|
|
765
|
+
const sessionID = this.readString(question.sessionID) ?? this.readString(question.sessionId);
|
|
766
|
+
if (!id || !sessionID)
|
|
767
|
+
return [];
|
|
768
|
+
return [{
|
|
769
|
+
id,
|
|
770
|
+
sessionID,
|
|
771
|
+
questions: Array.isArray(question.questions) ? question.questions : [],
|
|
772
|
+
tool: typeof question.tool === "object" && question.tool !== null ? question.tool : undefined,
|
|
773
|
+
}];
|
|
774
|
+
});
|
|
775
|
+
}
|
|
693
776
|
async refreshSessionsMetadata() {
|
|
694
777
|
const response = await this.client.session.list();
|
|
695
778
|
const sessions = Array.isArray(response.data) ? response.data : [];
|
package/dist/index.js
CHANGED
|
@@ -90,10 +90,13 @@ let aiManagerInitPromise = null;
|
|
|
90
90
|
// Proxy tunnel management
|
|
91
91
|
let currentSessionCode = null;
|
|
92
92
|
let currentSessionPassword = null;
|
|
93
|
+
let currentManagerSessionId = null;
|
|
93
94
|
let currentPrimaryGateway = DEFAULT_PROXY_URL;
|
|
94
95
|
let activeGatewayUrl = DEFAULT_PROXY_URL;
|
|
96
|
+
let appPeerConnected = false;
|
|
95
97
|
let shuttingDown = false;
|
|
96
98
|
let activeV2Transport = null;
|
|
99
|
+
const runningAiSessions = new Set();
|
|
97
100
|
const trackedEditorFiles = new Map();
|
|
98
101
|
const trackedEditorDirectories = new Map();
|
|
99
102
|
const pendingTrackedFileChecks = new Set();
|
|
@@ -302,7 +305,21 @@ async function readCliConfig() {
|
|
|
302
305
|
rootDir: entry.rootDir,
|
|
303
306
|
sessionCode: typeof entry.sessionCode === "string" ? entry.sessionCode : null,
|
|
304
307
|
sessionPassword: entry.sessionPassword,
|
|
308
|
+
managerSessionId: typeof entry.managerSessionId === "string" ? entry.managerSessionId : null,
|
|
305
309
|
savedAt: entry.savedAt,
|
|
310
|
+
pushDevices: Array.isArray(entry.pushDevices)
|
|
311
|
+
? entry.pushDevices.filter((device) => (!!device
|
|
312
|
+
&& typeof device.phoneId === "string"
|
|
313
|
+
&& typeof device.notificationsEnabled === "boolean"
|
|
314
|
+
&& typeof device.updatedAt === "number"
|
|
315
|
+
&& (typeof device.expoPushToken === "string" || device.expoPushToken === null))).map((device) => ({
|
|
316
|
+
phoneId: device.phoneId,
|
|
317
|
+
expoPushToken: device.expoPushToken,
|
|
318
|
+
notificationsEnabled: device.notificationsEnabled,
|
|
319
|
+
platform: typeof device.platform === "string" ? device.platform : null,
|
|
320
|
+
updatedAt: device.updatedAt,
|
|
321
|
+
}))
|
|
322
|
+
: [],
|
|
306
323
|
}))
|
|
307
324
|
: [],
|
|
308
325
|
};
|
|
@@ -317,7 +334,9 @@ async function readCliConfig() {
|
|
|
317
334
|
}
|
|
318
335
|
async function writeCliConfig(config) {
|
|
319
336
|
await fs.mkdir(path.dirname(CLI_CONFIG_PATH), { recursive: true });
|
|
320
|
-
|
|
337
|
+
const tmpPath = `${CLI_CONFIG_PATH}.${process.pid}.${Date.now()}.tmp`;
|
|
338
|
+
await fs.writeFile(tmpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
339
|
+
await fs.rename(tmpPath, CLI_CONFIG_PATH);
|
|
321
340
|
cliConfigPromise = Promise.resolve(config);
|
|
322
341
|
}
|
|
323
342
|
let cliConfigPromise = null;
|
|
@@ -331,14 +350,17 @@ function getSavedSessionForRoot(config, rootDir) {
|
|
|
331
350
|
const sessions = Array.isArray(config.sessions) ? config.sessions : [];
|
|
332
351
|
return sessions.find((entry) => entry.rootDir === rootDir) || null;
|
|
333
352
|
}
|
|
334
|
-
async function saveSessionForRoot(sessionCode, sessionPassword) {
|
|
353
|
+
async function saveSessionForRoot(sessionCode, sessionPassword, managerSessionId) {
|
|
335
354
|
const config = await getCliConfig();
|
|
336
355
|
const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
|
|
356
|
+
const previous = sessions.find((entry) => entry.rootDir === ROOT_DIR);
|
|
337
357
|
const nextEntry = {
|
|
338
358
|
rootDir: ROOT_DIR,
|
|
339
359
|
sessionCode,
|
|
340
360
|
sessionPassword,
|
|
361
|
+
managerSessionId,
|
|
341
362
|
savedAt: Date.now(),
|
|
363
|
+
pushDevices: previous?.pushDevices ?? [],
|
|
342
364
|
};
|
|
343
365
|
const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
|
|
344
366
|
deduped.unshift(nextEntry);
|
|
@@ -347,6 +369,192 @@ async function saveSessionForRoot(sessionCode, sessionPassword) {
|
|
|
347
369
|
sessions: deduped.slice(0, 100),
|
|
348
370
|
});
|
|
349
371
|
}
|
|
372
|
+
async function savePushDeviceForCurrentSession(payload) {
|
|
373
|
+
if (!currentSessionPassword) {
|
|
374
|
+
throw Object.assign(new Error("No active session"), { code: "EUNAVAILABLE" });
|
|
375
|
+
}
|
|
376
|
+
const phoneId = typeof payload.phoneId === "string" ? payload.phoneId.trim() : "";
|
|
377
|
+
if (!phoneId) {
|
|
378
|
+
throw Object.assign(new Error("phoneId is required"), { code: "EINVAL" });
|
|
379
|
+
}
|
|
380
|
+
const expoPushToken = typeof payload.expoPushToken === "string" && payload.expoPushToken.trim()
|
|
381
|
+
? payload.expoPushToken.trim()
|
|
382
|
+
: null;
|
|
383
|
+
const notificationsEnabled = payload.notificationsEnabled === true && Boolean(expoPushToken);
|
|
384
|
+
const platform = typeof payload.platform === "string" && payload.platform.trim()
|
|
385
|
+
? payload.platform.trim()
|
|
386
|
+
: null;
|
|
387
|
+
const config = await getCliConfig();
|
|
388
|
+
const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
|
|
389
|
+
const now = Date.now();
|
|
390
|
+
const currentEntry = sessions.find((entry) => entry.rootDir === ROOT_DIR) ?? {
|
|
391
|
+
rootDir: ROOT_DIR,
|
|
392
|
+
sessionCode: currentSessionCode,
|
|
393
|
+
sessionPassword: currentSessionPassword,
|
|
394
|
+
managerSessionId: currentManagerSessionId,
|
|
395
|
+
savedAt: now,
|
|
396
|
+
pushDevices: [],
|
|
397
|
+
};
|
|
398
|
+
const nextDevices = (currentEntry.pushDevices ?? []).filter((device) => device.phoneId !== phoneId);
|
|
399
|
+
nextDevices.unshift({
|
|
400
|
+
phoneId,
|
|
401
|
+
expoPushToken,
|
|
402
|
+
notificationsEnabled,
|
|
403
|
+
platform,
|
|
404
|
+
updatedAt: now,
|
|
405
|
+
});
|
|
406
|
+
const nextEntry = {
|
|
407
|
+
...currentEntry,
|
|
408
|
+
sessionCode: currentSessionCode,
|
|
409
|
+
sessionPassword: currentSessionPassword,
|
|
410
|
+
managerSessionId: currentManagerSessionId,
|
|
411
|
+
savedAt: now,
|
|
412
|
+
pushDevices: nextDevices.slice(0, 10),
|
|
413
|
+
};
|
|
414
|
+
const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
|
|
415
|
+
deduped.unshift(nextEntry);
|
|
416
|
+
await writeCliConfig({
|
|
417
|
+
...config,
|
|
418
|
+
sessions: deduped.slice(0, 100),
|
|
419
|
+
});
|
|
420
|
+
return { ok: true, notificationsEnabled, tokenStored: Boolean(expoPushToken) };
|
|
421
|
+
}
|
|
422
|
+
function readAiEventSessionId(event) {
|
|
423
|
+
const properties = event.properties || {};
|
|
424
|
+
const info = properties.info && typeof properties.info === "object" ? properties.info : {};
|
|
425
|
+
return ((typeof properties.sessionID === "string" && properties.sessionID)
|
|
426
|
+
|| (typeof properties.sessionId === "string" && properties.sessionId)
|
|
427
|
+
|| (typeof info.sessionID === "string" && info.sessionID)
|
|
428
|
+
|| (typeof info.sessionId === "string" && info.sessionId)
|
|
429
|
+
|| (typeof info.id === "string" && info.id)
|
|
430
|
+
|| null);
|
|
431
|
+
}
|
|
432
|
+
function readAiStatusType(status) {
|
|
433
|
+
if (typeof status === "string")
|
|
434
|
+
return status.toLowerCase();
|
|
435
|
+
if (status && typeof status === "object") {
|
|
436
|
+
const value = status.type;
|
|
437
|
+
if (typeof value === "string")
|
|
438
|
+
return value.toLowerCase();
|
|
439
|
+
}
|
|
440
|
+
return "";
|
|
441
|
+
}
|
|
442
|
+
function isRunningAiStatus(status) {
|
|
443
|
+
const value = readAiStatusType(status);
|
|
444
|
+
return value === "running" || value === "busy" || value === "working" || value === "processing" || value === "retry";
|
|
445
|
+
}
|
|
446
|
+
function isTerminalAiStatus(status) {
|
|
447
|
+
const value = readAiStatusType(status);
|
|
448
|
+
return (value === "idle"
|
|
449
|
+
|| value === "complete"
|
|
450
|
+
|| value === "completed"
|
|
451
|
+
|| value === "done"
|
|
452
|
+
|| value === "finished"
|
|
453
|
+
|| value === "error"
|
|
454
|
+
|| value === "failed"
|
|
455
|
+
|| value === "cancelled"
|
|
456
|
+
|| value === "canceled");
|
|
457
|
+
}
|
|
458
|
+
async function getCurrentNotificationDevices() {
|
|
459
|
+
const config = await getCliConfig();
|
|
460
|
+
const saved = getSavedSessionForRoot(config, ROOT_DIR);
|
|
461
|
+
if (!saved || saved.sessionPassword !== currentSessionPassword)
|
|
462
|
+
return [];
|
|
463
|
+
return (saved.pushDevices ?? []).filter((device) => (device.notificationsEnabled
|
|
464
|
+
&& typeof device.expoPushToken === "string"
|
|
465
|
+
&& device.expoPushToken.length > 0));
|
|
466
|
+
}
|
|
467
|
+
async function clearInvalidNotificationDevices(phoneIds) {
|
|
468
|
+
if (phoneIds.length === 0)
|
|
469
|
+
return;
|
|
470
|
+
const invalidPhoneIds = new Set(phoneIds);
|
|
471
|
+
const config = await getCliConfig();
|
|
472
|
+
const sessions = Array.isArray(config.sessions) ? [...config.sessions] : [];
|
|
473
|
+
const saved = getSavedSessionForRoot(config, ROOT_DIR);
|
|
474
|
+
if (!saved?.pushDevices?.length)
|
|
475
|
+
return;
|
|
476
|
+
const nextEntry = {
|
|
477
|
+
...saved,
|
|
478
|
+
pushDevices: saved.pushDevices.map((device) => (invalidPhoneIds.has(device.phoneId)
|
|
479
|
+
? { ...device, expoPushToken: null, notificationsEnabled: false, updatedAt: Date.now() }
|
|
480
|
+
: device)),
|
|
481
|
+
};
|
|
482
|
+
const deduped = sessions.filter((entry) => entry.rootDir !== ROOT_DIR);
|
|
483
|
+
deduped.unshift(nextEntry);
|
|
484
|
+
await writeCliConfig({
|
|
485
|
+
...config,
|
|
486
|
+
sessions: deduped.slice(0, 100),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async function notifyManagerAiCompletion(backend, aiSessionId) {
|
|
490
|
+
if (appPeerConnected)
|
|
491
|
+
return;
|
|
492
|
+
if (!currentManagerSessionId || !currentSessionPassword)
|
|
493
|
+
return;
|
|
494
|
+
const devices = await getCurrentNotificationDevices();
|
|
495
|
+
if (devices.length === 0)
|
|
496
|
+
return;
|
|
497
|
+
const response = await fetch(new URL("/v1/notifications/ai-complete", MANAGER_URL), {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: { "Content-Type": "application/json" },
|
|
500
|
+
signal: AbortSignal.timeout(10_000),
|
|
501
|
+
body: JSON.stringify({
|
|
502
|
+
sessionId: currentManagerSessionId,
|
|
503
|
+
resumeToken: currentSessionPassword,
|
|
504
|
+
backend,
|
|
505
|
+
aiSessionId,
|
|
506
|
+
devices: devices.map((device) => ({
|
|
507
|
+
phoneId: device.phoneId,
|
|
508
|
+
expoPushToken: device.expoPushToken,
|
|
509
|
+
platform: device.platform,
|
|
510
|
+
})),
|
|
511
|
+
}),
|
|
512
|
+
});
|
|
513
|
+
if (!response.ok) {
|
|
514
|
+
if (DEBUG_MODE) {
|
|
515
|
+
console.warn(`[notifications] manager notify failed: ${response.status}`);
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const body = await response.json().catch(() => ({}));
|
|
520
|
+
const invalidPhoneIds = Array.isArray(body.invalidPhoneIds)
|
|
521
|
+
? body.invalidPhoneIds.filter((value) => typeof value === "string")
|
|
522
|
+
: [];
|
|
523
|
+
if (invalidPhoneIds.length > 0) {
|
|
524
|
+
await clearInvalidNotificationDevices(invalidPhoneIds);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function handleAiNotificationEvent(backend, event) {
|
|
528
|
+
const sessionId = readAiEventSessionId(event);
|
|
529
|
+
if (!sessionId)
|
|
530
|
+
return;
|
|
531
|
+
const key = `${backend}:${sessionId}`;
|
|
532
|
+
if (event.type === "session.status") {
|
|
533
|
+
if (isRunningAiStatus(event.properties.status)) {
|
|
534
|
+
runningAiSessions.add(key);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (!isTerminalAiStatus(event.properties.status))
|
|
538
|
+
return;
|
|
539
|
+
if (!runningAiSessions.delete(key))
|
|
540
|
+
return;
|
|
541
|
+
void notifyManagerAiCompletion(backend, sessionId).catch((error) => {
|
|
542
|
+
if (DEBUG_MODE) {
|
|
543
|
+
console.warn("[notifications] failed to notify manager:", error instanceof Error ? error.message : String(error));
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (event.type !== "session.idle")
|
|
549
|
+
return;
|
|
550
|
+
if (!runningAiSessions.delete(key))
|
|
551
|
+
return;
|
|
552
|
+
void notifyManagerAiCompletion(backend, sessionId).catch((error) => {
|
|
553
|
+
if (DEBUG_MODE) {
|
|
554
|
+
console.warn("[notifications] failed to notify manager:", error instanceof Error ? error.message : String(error));
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
350
558
|
async function clearSavedSessionForRoot() {
|
|
351
559
|
const config = await getCliConfig();
|
|
352
560
|
const sessions = Array.isArray(config.sessions) ? config.sessions : [];
|
|
@@ -398,6 +606,51 @@ async function handleFsLs(payload) {
|
|
|
398
606
|
}
|
|
399
607
|
return { path: reqPath, entries: result };
|
|
400
608
|
}
|
|
609
|
+
async function handleFsSearchFiles(payload) {
|
|
610
|
+
const reqPath = payload.path || ".";
|
|
611
|
+
const query = typeof payload.query === "string" ? payload.query.trim().toLowerCase() : "";
|
|
612
|
+
const maxResults = Math.max(1, Math.min(payload.maxResults || 10, 10));
|
|
613
|
+
const safePath = assertSafePath(reqPath);
|
|
614
|
+
const rootIgnore = await loadGitignore(ROOT_DIR);
|
|
615
|
+
const matches = [];
|
|
616
|
+
async function searchDir(dirPath, relativePath, ig) {
|
|
617
|
+
if (matches.length >= maxResults)
|
|
618
|
+
return;
|
|
619
|
+
const localIgnore = ignore().add(ig);
|
|
620
|
+
try {
|
|
621
|
+
const localGitignorePath = path.join(dirPath, ".gitignore");
|
|
622
|
+
const content = await fs.readFile(localGitignorePath, "utf-8");
|
|
623
|
+
localIgnore.add(content);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
// No local .gitignore
|
|
627
|
+
}
|
|
628
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
629
|
+
for (const entry of entries) {
|
|
630
|
+
if (matches.length >= maxResults)
|
|
631
|
+
break;
|
|
632
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
633
|
+
const checkPath = entry.isDirectory() ? `${relPath}/` : relPath;
|
|
634
|
+
if (localIgnore.ignores(checkPath))
|
|
635
|
+
continue;
|
|
636
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
637
|
+
if (entry.isDirectory()) {
|
|
638
|
+
await searchDir(fullPath, relPath, localIgnore);
|
|
639
|
+
}
|
|
640
|
+
else if (entry.isFile() && relPath.toLowerCase().includes(query)) {
|
|
641
|
+
matches.push({ path: relPath });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const stat = await fs.stat(safePath);
|
|
646
|
+
if (stat.isDirectory()) {
|
|
647
|
+
await searchDir(safePath, reqPath === "." ? "" : reqPath, rootIgnore);
|
|
648
|
+
}
|
|
649
|
+
else if (stat.isFile() && reqPath.toLowerCase().includes(query)) {
|
|
650
|
+
matches.push({ path: reqPath });
|
|
651
|
+
}
|
|
652
|
+
return { path: reqPath, query, maxResults, files: matches };
|
|
653
|
+
}
|
|
401
654
|
async function handleFsStat(payload) {
|
|
402
655
|
const reqPath = payload.path;
|
|
403
656
|
if (!reqPath)
|
|
@@ -1488,6 +1741,7 @@ function handleSystemCapabilities() {
|
|
|
1488
1741
|
platform: os.platform(),
|
|
1489
1742
|
rootDir: ROOT_DIR,
|
|
1490
1743
|
hostname: os.hostname(),
|
|
1744
|
+
managerSessionId: currentManagerSessionId,
|
|
1491
1745
|
};
|
|
1492
1746
|
}
|
|
1493
1747
|
function handleSystemPing() {
|
|
@@ -2461,6 +2715,9 @@ async function processMessage(message) {
|
|
|
2461
2715
|
case "ping":
|
|
2462
2716
|
result = handleSystemPing();
|
|
2463
2717
|
break;
|
|
2718
|
+
case "setPushToken":
|
|
2719
|
+
result = await savePushDeviceForCurrentSession(payload);
|
|
2720
|
+
break;
|
|
2464
2721
|
case "pairDevice": {
|
|
2465
2722
|
throw Object.assign(new Error("pairDevice is no longer supported"), { code: "EINVAL" });
|
|
2466
2723
|
}
|
|
@@ -2473,6 +2730,9 @@ async function processMessage(message) {
|
|
|
2473
2730
|
case "ls":
|
|
2474
2731
|
result = await handleFsLs(payload);
|
|
2475
2732
|
break;
|
|
2733
|
+
case "searchFiles":
|
|
2734
|
+
result = await handleFsSearchFiles(payload);
|
|
2735
|
+
break;
|
|
2476
2736
|
case "stat":
|
|
2477
2737
|
result = await handleFsStat(payload);
|
|
2478
2738
|
break;
|
|
@@ -2668,6 +2928,14 @@ async function processMessage(message) {
|
|
|
2668
2928
|
case "listSessions":
|
|
2669
2929
|
result = await aiManager.listAllSessions();
|
|
2670
2930
|
break;
|
|
2931
|
+
case "syncState": {
|
|
2932
|
+
const rawSessionIds = payload.sessionIds;
|
|
2933
|
+
result = await aiManager.syncState({
|
|
2934
|
+
opencode: Array.isArray(rawSessionIds?.opencode) ? rawSessionIds.opencode.filter((id) => typeof id === "string") : undefined,
|
|
2935
|
+
codex: Array.isArray(rawSessionIds?.codex) ? rawSessionIds.codex.filter((id) => typeof id === "string") : undefined,
|
|
2936
|
+
});
|
|
2937
|
+
break;
|
|
2938
|
+
}
|
|
2671
2939
|
case "getSession":
|
|
2672
2940
|
result = await aiManager.getSession(backend, payload.id);
|
|
2673
2941
|
break;
|
|
@@ -2839,7 +3107,11 @@ async function assembleWithCode(code) {
|
|
|
2839
3107
|
return;
|
|
2840
3108
|
settled = true;
|
|
2841
3109
|
ws.send(JSON.stringify({ type: "ack" }));
|
|
2842
|
-
resolve({
|
|
3110
|
+
resolve({
|
|
3111
|
+
code: parsed.code,
|
|
3112
|
+
password: parsed.password,
|
|
3113
|
+
sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId : null,
|
|
3114
|
+
});
|
|
2843
3115
|
}
|
|
2844
3116
|
catch (error) {
|
|
2845
3117
|
fail(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -3101,6 +3373,7 @@ function startAiManagerInBackground() {
|
|
|
3101
3373
|
}
|
|
3102
3374
|
aiManager = manager;
|
|
3103
3375
|
aiManager.subscribe((backend, event) => {
|
|
3376
|
+
handleAiNotificationEvent(backend, event);
|
|
3104
3377
|
emitAppEvent({
|
|
3105
3378
|
v: 1,
|
|
3106
3379
|
id: `evt-${Date.now()}`,
|
|
@@ -3140,16 +3413,19 @@ async function connectWebSocketV2() {
|
|
|
3140
3413
|
if (message.type === "connected")
|
|
3141
3414
|
return;
|
|
3142
3415
|
if (message.type === "peer_connected") {
|
|
3416
|
+
appPeerConnected = true;
|
|
3143
3417
|
console.log("App connected!\n");
|
|
3144
3418
|
void publishDiscoveredPorts(true);
|
|
3145
3419
|
return;
|
|
3146
3420
|
}
|
|
3147
3421
|
if (message.type === "peer_disconnected") {
|
|
3422
|
+
appPeerConnected = false;
|
|
3148
3423
|
console.log("App disconnected. Waiting for reconnect window.\n");
|
|
3149
3424
|
stopPortSync();
|
|
3150
3425
|
return;
|
|
3151
3426
|
}
|
|
3152
3427
|
if (message.type === "app_disconnected") {
|
|
3428
|
+
appPeerConnected = false;
|
|
3153
3429
|
if (message.reconnectDeadline) {
|
|
3154
3430
|
console.log(`[session] app disconnected, waiting until ${new Date(message.reconnectDeadline).toISOString()}`);
|
|
3155
3431
|
}
|
|
@@ -3254,6 +3530,7 @@ async function main() {
|
|
|
3254
3530
|
displaySavedSessionNotice();
|
|
3255
3531
|
sessionCodeToUse = savedSession.sessionCode;
|
|
3256
3532
|
sessionPasswordToUse = savedSession.sessionPassword;
|
|
3533
|
+
currentManagerSessionId = savedSession.managerSessionId ?? null;
|
|
3257
3534
|
usedSavedSession = true;
|
|
3258
3535
|
}
|
|
3259
3536
|
else {
|
|
@@ -3267,7 +3544,8 @@ async function main() {
|
|
|
3267
3544
|
const assembled = await assembleWithCode(qr.code);
|
|
3268
3545
|
sessionCodeToUse = assembled.code;
|
|
3269
3546
|
sessionPasswordToUse = assembled.password;
|
|
3270
|
-
|
|
3547
|
+
currentManagerSessionId = assembled.sessionId;
|
|
3548
|
+
await saveSessionForRoot(sessionCodeToUse, sessionPasswordToUse, currentManagerSessionId);
|
|
3271
3549
|
}
|
|
3272
3550
|
currentSessionCode = sessionCodeToUse;
|
|
3273
3551
|
currentSessionPassword = sessionPasswordToUse;
|