@prestyj/cli 4.10.2 → 4.11.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.
- package/README.md +4 -4
- package/dist/app-sidecar.d.ts +2 -0
- package/dist/app-sidecar.d.ts.map +1 -0
- package/dist/app-sidecar.js +1407 -0
- package/dist/app-sidecar.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -22
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session.d.ts +120 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +412 -6
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-providers.d.ts +18 -0
- package/dist/core/auth-providers.d.ts.map +1 -0
- package/dist/core/auth-providers.js +71 -0
- package/dist/core/auth-providers.js.map +1 -0
- package/dist/core/event-bus.d.ts +6 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +1 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/project-discovery.d.ts +41 -0
- package/dist/core/project-discovery.d.ts.map +1 -0
- package/dist/core/project-discovery.js +441 -0
- package/dist/core/project-discovery.js.map +1 -0
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +91 -0
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +17 -0
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/radio.d.ts +44 -0
- package/dist/core/radio.d.ts.map +1 -0
- package/dist/core/radio.js +250 -0
- package/dist/core/radio.js.map +1 -0
- package/dist/core/resolve-start.d.ts +29 -0
- package/dist/core/resolve-start.d.ts.map +1 -0
- package/dist/core/resolve-start.js +35 -0
- package/dist/core/resolve-start.js.map +1 -0
- package/dist/core/resolve-start.test.d.ts +2 -0
- package/dist/core/resolve-start.test.d.ts.map +1 -0
- package/dist/core/resolve-start.test.js +66 -0
- package/dist/core/resolve-start.test.js.map +1 -0
- package/dist/core/settings-manager.d.ts +1 -1
- package/dist/core/shell-path.d.ts +12 -0
- package/dist/core/shell-path.d.ts.map +1 -0
- package/dist/core/shell-path.js +166 -0
- package/dist/core/shell-path.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +10 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/telegram-config.d.ts +21 -0
- package/dist/core/telegram-config.d.ts.map +1 -0
- package/dist/core/telegram-config.js +52 -0
- package/dist/core/telegram-config.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/modes/serve-mode.d.ts +24 -0
- package/dist/modes/serve-mode.d.ts.map +1 -1
- package/dist/modes/serve-mode.js +54 -19
- package/dist/modes/serve-mode.js.map +1 -1
- package/dist/tools/goal-mode.test.js +1 -1
- package/dist/tools/goal-mode.test.js.map +1 -1
- package/dist/tools/goals.d.ts +7 -7
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/subagent.d.ts +2 -4
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +24 -23
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/tasks.d.ts +1 -1
- package/dist/ui/hooks/useTranscriptHistory.d.ts +2 -0
- package/dist/ui/hooks/useTranscriptHistory.d.ts.map +1 -1
- package/dist/ui/hooks/useTranscriptHistory.js +3 -2
- package/dist/ui/hooks/useTranscriptHistory.js.map +1 -1
- package/dist/ui/long-prompt-regression-harness.test.js +8 -8
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +60 -8
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/startup-banner-order.test.js +1 -1
- package/dist/ui/startup-banner-order.test.js.map +1 -1
- package/dist/ui/terminal-history-repaint.test.d.ts +2 -0
- package/dist/ui/terminal-history-repaint.test.d.ts.map +1 -0
- package/dist/ui/terminal-history-repaint.test.js +73 -0
- package/dist/ui/terminal-history-repaint.test.js.map +1 -0
- package/dist/ui/terminal-history.d.ts +1 -0
- package/dist/ui/terminal-history.d.ts.map +1 -1
- package/dist/ui/terminal-history.js +31 -2
- package/dist/ui/terminal-history.js.map +1 -1
- package/dist/utils/git.d.ts +6 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +12 -0
- package/dist/utils/git.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type AuthMethod = "oauth" | "apikey";
|
|
2
|
+
export interface AuthProviderMeta {
|
|
3
|
+
/** Stable provider id (matches the gg-ai Provider union, plus storage keys). */
|
|
4
|
+
value: string;
|
|
5
|
+
/** Display name shown in the login list. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** One-line model summary. */
|
|
8
|
+
description: string;
|
|
9
|
+
/** Supported auth methods, in preferred order (oauth first when both). */
|
|
10
|
+
methods: AuthMethod[];
|
|
11
|
+
/** Friendly label for the API key field (e.g. "Z.AI"). */
|
|
12
|
+
apiKeyLabel?: string;
|
|
13
|
+
/** Fixed base URL stored alongside an API key (e.g. Xiaomi's token plan). */
|
|
14
|
+
apiKeyBaseUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const AUTH_PROVIDERS: AuthProviderMeta[];
|
|
17
|
+
export declare function getAuthProvider(value: string): AuthProviderMeta | undefined;
|
|
18
|
+
//# sourceMappingURL=auth-providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-providers.d.ts","sourceRoot":"","sources":["../../src/core/auth-providers.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5C,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,EAAE,gBAAgB,EA8D5C,CAAC;AAEF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAE3E"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Single source of truth for the providers the login UI offers and how each
|
|
2
|
+
// authenticates. Mirrors the CLI's `ezcoder login` provider list (ui/login.tsx)
|
|
3
|
+
// so the desktop app and the terminal stay in lockstep. The app fetches this
|
|
4
|
+
// (plus live connection status) from the sidecar's /auth/status endpoint.
|
|
5
|
+
export const AUTH_PROVIDERS = [
|
|
6
|
+
{
|
|
7
|
+
value: "anthropic",
|
|
8
|
+
label: "Anthropic",
|
|
9
|
+
description: "Claude Opus 4.8, Sonnet 4.6, Haiku 4.5",
|
|
10
|
+
methods: ["oauth"],
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
value: "openai",
|
|
14
|
+
label: "OpenAI",
|
|
15
|
+
description: "GPT-5.5, GPT-5.5 Pro, GPT-5.4, GPT-5.3 Codex",
|
|
16
|
+
methods: ["oauth"],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
value: "gemini",
|
|
20
|
+
label: "Gemini",
|
|
21
|
+
description: "Gemini 3.1 Flash Lite Preview",
|
|
22
|
+
methods: ["oauth"],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
value: "moonshot",
|
|
26
|
+
label: "Moonshot",
|
|
27
|
+
description: "Kimi K2.7 · OAuth or API key",
|
|
28
|
+
methods: ["oauth", "apikey"],
|
|
29
|
+
apiKeyLabel: "Moonshot",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
value: "glm",
|
|
33
|
+
label: "Z.AI (GLM)",
|
|
34
|
+
description: "GLM-5.1, GLM-4.7, GLM-4.7 Flash",
|
|
35
|
+
methods: ["apikey"],
|
|
36
|
+
apiKeyLabel: "Z.AI",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: "minimax",
|
|
40
|
+
label: "MiniMax",
|
|
41
|
+
description: "MiniMax M3",
|
|
42
|
+
methods: ["apikey"],
|
|
43
|
+
apiKeyLabel: "MiniMax",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
value: "xiaomi",
|
|
47
|
+
label: "Xiaomi (MiMo)",
|
|
48
|
+
description: "MiMo-V2-Pro",
|
|
49
|
+
methods: ["apikey"],
|
|
50
|
+
apiKeyLabel: "Xiaomi MiMo",
|
|
51
|
+
apiKeyBaseUrl: "https://token-plan-sgp.xiaomimimo.com/v1",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
value: "deepseek",
|
|
55
|
+
label: "DeepSeek",
|
|
56
|
+
description: "DeepSeek V4 Pro, V4 Flash",
|
|
57
|
+
methods: ["apikey"],
|
|
58
|
+
apiKeyLabel: "DeepSeek",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
value: "openrouter",
|
|
62
|
+
label: "OpenRouter",
|
|
63
|
+
description: "Qwen3.6-Plus, multi-provider gateway",
|
|
64
|
+
methods: ["apikey"],
|
|
65
|
+
apiKeyLabel: "OpenRouter",
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
export function getAuthProvider(value) {
|
|
69
|
+
return AUTH_PROVIDERS.find((p) => p.value === value);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=auth-providers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-providers.js","sourceRoot":"","sources":["../../src/core/auth-providers.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,gFAAgF;AAChF,6EAA6E;AAC7E,0EAA0E;AAmB1E,MAAM,CAAC,MAAM,cAAc,GAAuB;IAChD;QACE,KAAK,EAAE,WAAW;QAClB,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,wCAAwC;QACrD,OAAO,EAAE,CAAC,OAAO,CAAC;KACnB;IACD;QACE,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE,QAAQ;QACf,WAAW,EAAE,8CAA8C;QAC3D,OAAO,EAAE,CAAC,OAAO,CAAC;KACnB;IACD;QACE,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE,QAAQ;QACf,WAAW,EAAE,+BAA+B;QAC5C,OAAO,EAAE,CAAC,OAAO,CAAC;KACnB;IACD;QACE,KAAK,EAAE,UAAU;QACjB,KAAK,EAAE,UAAU;QACjB,WAAW,EAAE,8BAA8B;QAC3C,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC;QAC5B,WAAW,EAAE,UAAU;KACxB;IACD;QACE,KAAK,EAAE,KAAK;QACZ,KAAK,EAAE,YAAY;QACnB,WAAW,EAAE,iCAAiC;QAC9C,OAAO,EAAE,CAAC,QAAQ,CAAC;QACnB,WAAW,EAAE,MAAM;KACpB;IACD;QACE,KAAK,EAAE,SAAS;QAChB,KAAK,EAAE,SAAS;QAChB,WAAW,EAAE,YAAY;QACzB,OAAO,EAAE,CAAC,QAAQ,CAAC;QACnB,WAAW,EAAE,SAAS;KACvB;IACD;QACE,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE,eAAe;QACtB,WAAW,EAAE,aAAa;QAC1B,OAAO,EAAE,CAAC,QAAQ,CAAC;QACnB,WAAW,EAAE,aAAa;QAC1B,aAAa,EAAE,0CAA0C;KAC1D;IACD;QACE,KAAK,EAAE,UAAU;QACjB,KAAK,EAAE,UAAU;QACjB,WAAW,EAAE,2BAA2B;QACxC,OAAO,EAAE,CAAC,QAAQ,CAAC;QACnB,WAAW,EAAE,UAAU;KACxB;IACD;QACE,KAAK,EAAE,YAAY;QACnB,KAAK,EAAE,YAAY;QACnB,WAAW,EAAE,sCAAsC;QACnD,OAAO,EAAE,CAAC,QAAQ,CAAC;QACnB,WAAW,EAAE,YAAY;KAC1B;CACF,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;AACvD,CAAC"}
|
package/dist/core/event-bus.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface BusEventMap {
|
|
|
20
20
|
result: string;
|
|
21
21
|
isError: boolean;
|
|
22
22
|
durationMs: number;
|
|
23
|
+
/** Tool-specific extras (e.g. screenshot/read image previews). */
|
|
24
|
+
details?: unknown;
|
|
23
25
|
};
|
|
24
26
|
turn_end: {
|
|
25
27
|
turn: number;
|
|
@@ -53,12 +55,16 @@ export interface BusEventMap {
|
|
|
53
55
|
resultType: string;
|
|
54
56
|
data: unknown;
|
|
55
57
|
};
|
|
58
|
+
hook: {
|
|
59
|
+
kind: "ideal" | "loop_break" | "regrounding";
|
|
60
|
+
};
|
|
56
61
|
session_start: {
|
|
57
62
|
sessionId: string;
|
|
58
63
|
};
|
|
59
64
|
model_change: {
|
|
60
65
|
provider: string;
|
|
61
66
|
model: string;
|
|
67
|
+
supportsVideo?: boolean;
|
|
62
68
|
};
|
|
63
69
|
compaction_start: {
|
|
64
70
|
messageCount: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/core/event-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAIjD,MAAM,WAAW,WAAW;IAE1B,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7B,cAAc,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,eAAe,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IACrF,gBAAgB,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IAC1D,aAAa,EAAE;
|
|
1
|
+
{"version":3,"file":"event-bus.d.ts","sourceRoot":"","sources":["../../src/core/event-bus.ts"],"names":[],"mappings":"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"}
|
package/dist/core/event-bus.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../../src/core/event-bus.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../../src/core/event-bus.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type ProjectSource = "ezcoder" | "claude-code" | "codex";
|
|
2
|
+
export interface DiscoveredProject {
|
|
3
|
+
name: string;
|
|
4
|
+
path: string;
|
|
5
|
+
lastActiveMs: number;
|
|
6
|
+
lastActiveDisplay: string;
|
|
7
|
+
/** Sorted, deduped list of stores this project showed up in. */
|
|
8
|
+
sources: ProjectSource[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Scan ezcoder + Claude Code + Codex session stores and return one row per
|
|
12
|
+
* project, sorted most-recent first. Duplicates (same cwd) are collapsed; the
|
|
13
|
+
* `sources` field lists every store the project appeared in so the picker can
|
|
14
|
+
* show a combined badge.
|
|
15
|
+
*/
|
|
16
|
+
export declare function discoverProjects(): Promise<DiscoveredProject[]>;
|
|
17
|
+
export interface RecentSession {
|
|
18
|
+
/** Session id. */
|
|
19
|
+
id: string;
|
|
20
|
+
/** Absolute path to the session .jsonl (passed back to reopen it). */
|
|
21
|
+
path: string;
|
|
22
|
+
/** First user message, trimmed to a short preview (may be empty). */
|
|
23
|
+
preview: string;
|
|
24
|
+
/** Relative "3h ago" string from last activity. */
|
|
25
|
+
lastActiveDisplay: string;
|
|
26
|
+
messageCount: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* List the most recent ezcoder sessions for a project cwd, newest first, each
|
|
30
|
+
* with a short preview built from its first user message. Used by the new-window
|
|
31
|
+
* project picker to offer "resume a session" alongside "new session".
|
|
32
|
+
*
|
|
33
|
+
* Fast path: instead of fully reading every session file in the project (what
|
|
34
|
+
* SessionManager.list does to count messages), sort files by mtime and read
|
|
35
|
+
* only the newest `limit`. Each chosen file is parsed in ONE pass for its
|
|
36
|
+
* header id, message count, last activity, and first user preview. For projects
|
|
37
|
+
* with many/large sessions this is the difference between scanning everything
|
|
38
|
+
* and scanning ~5 files.
|
|
39
|
+
*/
|
|
40
|
+
export declare function listRecentSessions(cwd: string, limit?: number): Promise<RecentSession[]>;
|
|
41
|
+
//# sourceMappingURL=project-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-discovery.d.ts","sourceRoot":"","sources":["../../src/core/project-discovery.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,aAAa,GAAG,OAAO,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA6BrE;AAsSD,MAAM,WAAW,aAAa;IAC5B,kBAAkB;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAczF"}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { getAppPaths } from "../config.js";
|
|
7
|
+
/**
|
|
8
|
+
* Scan ezcoder + Claude Code + Codex session stores and return one row per
|
|
9
|
+
* project, sorted most-recent first. Duplicates (same cwd) are collapsed; the
|
|
10
|
+
* `sources` field lists every store the project appeared in so the picker can
|
|
11
|
+
* show a combined badge.
|
|
12
|
+
*/
|
|
13
|
+
export async function discoverProjects() {
|
|
14
|
+
const [gg, cc, cx] = await Promise.all([
|
|
15
|
+
discoverGgcoderProjects(),
|
|
16
|
+
discoverClaudeProjects(),
|
|
17
|
+
discoverCodexProjects(),
|
|
18
|
+
]);
|
|
19
|
+
const byPath = new Map();
|
|
20
|
+
for (const p of [...gg, ...cc, ...cx]) {
|
|
21
|
+
const existing = byPath.get(p.path);
|
|
22
|
+
if (!existing) {
|
|
23
|
+
byPath.set(p.path, p);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
byPath.set(p.path, {
|
|
27
|
+
name: existing.name,
|
|
28
|
+
path: existing.path,
|
|
29
|
+
lastActiveMs: Math.max(existing.lastActiveMs, p.lastActiveMs),
|
|
30
|
+
lastActiveDisplay: "", // recomputed below
|
|
31
|
+
sources: mergeSources(existing.sources, p.sources),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const merged = Array.from(byPath.values()).map((p) => ({
|
|
35
|
+
...p,
|
|
36
|
+
lastActiveDisplay: formatRelativeTime(p.lastActiveMs),
|
|
37
|
+
}));
|
|
38
|
+
merged.sort((a, b) => b.lastActiveMs - a.lastActiveMs);
|
|
39
|
+
return merged;
|
|
40
|
+
}
|
|
41
|
+
const SOURCE_ORDER = {
|
|
42
|
+
ezcoder: 0,
|
|
43
|
+
"claude-code": 1,
|
|
44
|
+
codex: 2,
|
|
45
|
+
};
|
|
46
|
+
function mergeSources(a, b) {
|
|
47
|
+
const set = new Set([...a, ...b]);
|
|
48
|
+
return Array.from(set).sort((x, y) => SOURCE_ORDER[x] - SOURCE_ORDER[y]);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Scan ~/.ezcoder/sessions/. Each session directory's name is the encoded cwd
|
|
52
|
+
* (slashes → underscores); we decode it back and verify the directory still
|
|
53
|
+
* exists on disk.
|
|
54
|
+
*/
|
|
55
|
+
async function discoverGgcoderProjects() {
|
|
56
|
+
const sessionsDir = getAppPaths().sessionsDir;
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = await fs.readdir(sessionsDir);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const results = [];
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const dir = path.join(sessionsDir, entry);
|
|
67
|
+
const mtime = await maxJsonlMtime(dir);
|
|
68
|
+
if (mtime === null)
|
|
69
|
+
continue;
|
|
70
|
+
// Normalize so paths containing traversal segments (e.g. an agent launched
|
|
71
|
+
// with cwd `.../src-tauri/../..`) collapse to their real directory and the
|
|
72
|
+
// basename isn't a stray "..". Deduped against other entries downstream.
|
|
73
|
+
const decoded = path.resolve("/" + entry.replace(/_/g, "/"));
|
|
74
|
+
if (!(await isDirectory(decoded)))
|
|
75
|
+
continue;
|
|
76
|
+
results.push({
|
|
77
|
+
name: path.basename(decoded),
|
|
78
|
+
path: decoded,
|
|
79
|
+
lastActiveMs: mtime,
|
|
80
|
+
lastActiveDisplay: formatRelativeTime(mtime),
|
|
81
|
+
sources: ["ezcoder"],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Scan ~/.claude/projects/. Claude Code's directory encoding replaces every
|
|
88
|
+
* "/" with "-", which is genuinely ambiguous — a real dash in a path component
|
|
89
|
+
* (e.g. "ezcoder") collides with the separator. So we extract the cwd from
|
|
90
|
+
* the JSONL events themselves; Claude writes it into user/assistant records.
|
|
91
|
+
* Falls back to a best-effort dash decode only if no event carries a cwd.
|
|
92
|
+
*/
|
|
93
|
+
async function discoverClaudeProjects() {
|
|
94
|
+
const projectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
95
|
+
let entries;
|
|
96
|
+
try {
|
|
97
|
+
entries = await fs.readdir(projectsDir);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const results = await Promise.all(entries.map(async (entry) => {
|
|
103
|
+
const dir = path.join(projectsDir, entry);
|
|
104
|
+
const mtime = await maxJsonlMtime(dir);
|
|
105
|
+
if (mtime === null)
|
|
106
|
+
return null;
|
|
107
|
+
const cwd = (await readFirstFromJsonlDir(dir, claudeCwdExtractor)) ?? fallbackDashDecode(entry);
|
|
108
|
+
if (!cwd)
|
|
109
|
+
return null;
|
|
110
|
+
if (!(await isDirectory(cwd)))
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
name: path.basename(cwd),
|
|
114
|
+
path: cwd,
|
|
115
|
+
lastActiveMs: mtime,
|
|
116
|
+
lastActiveDisplay: formatRelativeTime(mtime),
|
|
117
|
+
sources: ["claude-code"],
|
|
118
|
+
};
|
|
119
|
+
}));
|
|
120
|
+
return results.filter((p) => p !== null);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Scan ~/.codex/sessions/. Codex stores sessions flat by date
|
|
124
|
+
* (`YYYY/MM/DD/rollout-*.jsonl`) with the cwd embedded in the first user
|
|
125
|
+
* message as `<environment_context><cwd>/abs/path</cwd>...</environment_context>`.
|
|
126
|
+
* We group sessions by extracted cwd and take max mtime per group.
|
|
127
|
+
*/
|
|
128
|
+
async function discoverCodexProjects() {
|
|
129
|
+
const sessionsDir = path.join(os.homedir(), ".codex", "sessions");
|
|
130
|
+
if (!(await isDirectory(sessionsDir)))
|
|
131
|
+
return [];
|
|
132
|
+
// Layout is YYYY/MM/DD/*.jsonl — depth 4 covers it.
|
|
133
|
+
const files = await collectJsonlFiles(sessionsDir, 4);
|
|
134
|
+
if (files.length === 0)
|
|
135
|
+
return [];
|
|
136
|
+
// Process newest first so per-cwd we always start with the latest mtime.
|
|
137
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
138
|
+
const byCwd = new Map();
|
|
139
|
+
for (const f of files) {
|
|
140
|
+
const cwd = await readFirstFromFile(f.path, codexCwdExtractor);
|
|
141
|
+
if (!cwd)
|
|
142
|
+
continue;
|
|
143
|
+
const prev = byCwd.get(cwd);
|
|
144
|
+
if (prev === undefined || f.mtime > prev)
|
|
145
|
+
byCwd.set(cwd, f.mtime);
|
|
146
|
+
}
|
|
147
|
+
const results = [];
|
|
148
|
+
for (const [cwd, mtime] of byCwd) {
|
|
149
|
+
if (!(await isDirectory(cwd)))
|
|
150
|
+
continue;
|
|
151
|
+
results.push({
|
|
152
|
+
name: path.basename(cwd),
|
|
153
|
+
path: cwd,
|
|
154
|
+
lastActiveMs: mtime,
|
|
155
|
+
lastActiveDisplay: formatRelativeTime(mtime),
|
|
156
|
+
sources: ["codex"],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
async function isDirectory(p) {
|
|
162
|
+
try {
|
|
163
|
+
const s = await fs.stat(p);
|
|
164
|
+
return s.isDirectory();
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function maxJsonlMtime(dir) {
|
|
171
|
+
if (!(await isDirectory(dir)))
|
|
172
|
+
return null;
|
|
173
|
+
const files = await collectJsonlFiles(dir, 2);
|
|
174
|
+
if (files.length === 0)
|
|
175
|
+
return null;
|
|
176
|
+
let max = 0;
|
|
177
|
+
for (const f of files)
|
|
178
|
+
if (f.mtime > max)
|
|
179
|
+
max = f.mtime;
|
|
180
|
+
return max > 0 ? max : null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Walk `dir` up to `maxDepth` levels deep collecting every .jsonl file. Used
|
|
184
|
+
* for both Claude Code (top-level + `<uuid>/subagents/`) and Codex
|
|
185
|
+
* (`YYYY/MM/DD/`) layouts.
|
|
186
|
+
*/
|
|
187
|
+
async function collectJsonlFiles(dir, maxDepth) {
|
|
188
|
+
const out = [];
|
|
189
|
+
await walk(dir, 0);
|
|
190
|
+
return out;
|
|
191
|
+
async function walk(current, depth) {
|
|
192
|
+
let entries;
|
|
193
|
+
try {
|
|
194
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
for (const e of entries) {
|
|
200
|
+
const full = path.join(current, e.name);
|
|
201
|
+
if (e.isFile() && e.name.endsWith(".jsonl")) {
|
|
202
|
+
try {
|
|
203
|
+
const s = await fs.stat(full);
|
|
204
|
+
out.push({ path: full, mtime: s.mtimeMs });
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// skip unreadable
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (e.isDirectory() && depth < maxDepth) {
|
|
211
|
+
await walk(full, depth + 1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const claudeCwdExtractor = (line) => {
|
|
217
|
+
try {
|
|
218
|
+
const parsed = JSON.parse(line);
|
|
219
|
+
if (typeof parsed.cwd === "string" && parsed.cwd.startsWith("/"))
|
|
220
|
+
return parsed.cwd;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// skip malformed
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
};
|
|
227
|
+
const CODEX_CWD_RE = /<cwd>([^<]+)<\/cwd>/;
|
|
228
|
+
const codexCwdExtractor = (line) => {
|
|
229
|
+
// Current format (openai/codex protocol.rs, late-2025+): RolloutLine wraps
|
|
230
|
+
// SessionMeta / TurnContext items with `{ type, payload: { cwd, ... } }`.
|
|
231
|
+
// First line is always SessionMeta, so this hits on read 1.
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(line);
|
|
234
|
+
const cwd = parsed.payload?.cwd;
|
|
235
|
+
if (typeof cwd === "string" && cwd.startsWith("/"))
|
|
236
|
+
return cwd;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// not JSON or unexpected shape; fall through to legacy regex
|
|
240
|
+
}
|
|
241
|
+
// Legacy format (pre-late-2025): cwd embedded as <cwd>...</cwd> inside an
|
|
242
|
+
// <environment_context> user-message string.
|
|
243
|
+
const m = CODEX_CWD_RE.exec(line);
|
|
244
|
+
if (m && m[1] && m[1].startsWith("/"))
|
|
245
|
+
return m[1];
|
|
246
|
+
return null;
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* Walk all .jsonl files under `dir` newest-first, returning the first non-null
|
|
250
|
+
* extractor result. Walks two levels deep (matches Claude Code's nested
|
|
251
|
+
* layout).
|
|
252
|
+
*/
|
|
253
|
+
async function readFirstFromJsonlDir(dir, extractor) {
|
|
254
|
+
const files = await collectJsonlFiles(dir, 2);
|
|
255
|
+
if (files.length === 0)
|
|
256
|
+
return null;
|
|
257
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
258
|
+
for (const f of files) {
|
|
259
|
+
const v = await readFirstFromFile(f.path, extractor);
|
|
260
|
+
if (v)
|
|
261
|
+
return v;
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Stream `file` line-by-line and return the first non-null extractor result.
|
|
267
|
+
* Caps lines so a giant transcript can't stall discovery.
|
|
268
|
+
*/
|
|
269
|
+
async function readFirstFromFile(file, extractor) {
|
|
270
|
+
return new Promise((resolve) => {
|
|
271
|
+
const stream = createReadStream(file, { encoding: "utf-8" });
|
|
272
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
273
|
+
let lines = 0;
|
|
274
|
+
let done = false;
|
|
275
|
+
const MAX_LINES = 200;
|
|
276
|
+
// Resolve before tearing down. rl.close() synchronously emits 'close',
|
|
277
|
+
// and if the close handler resolves first our real value gets swallowed.
|
|
278
|
+
const finish = (value) => {
|
|
279
|
+
if (done)
|
|
280
|
+
return;
|
|
281
|
+
done = true;
|
|
282
|
+
resolve(value);
|
|
283
|
+
rl.close();
|
|
284
|
+
stream.destroy();
|
|
285
|
+
};
|
|
286
|
+
rl.on("line", (line) => {
|
|
287
|
+
if (done)
|
|
288
|
+
return;
|
|
289
|
+
lines++;
|
|
290
|
+
if (lines > MAX_LINES) {
|
|
291
|
+
finish(null);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const v = extractor(line);
|
|
295
|
+
if (v)
|
|
296
|
+
finish(v);
|
|
297
|
+
});
|
|
298
|
+
rl.on("close", () => finish(null));
|
|
299
|
+
rl.on("error", () => finish(null));
|
|
300
|
+
stream.on("error", () => finish(null));
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function fallbackDashDecode(entry) {
|
|
304
|
+
// Strip leading "-" then turn remaining "-" into "/". Lossy by design — only
|
|
305
|
+
// used when the JSONLs have no cwd events; the caller still verifies the
|
|
306
|
+
// result is an existing directory.
|
|
307
|
+
if (!entry.startsWith("-"))
|
|
308
|
+
return null;
|
|
309
|
+
return "/" + entry.slice(1).replace(/-/g, "/");
|
|
310
|
+
}
|
|
311
|
+
function formatRelativeTime(ms) {
|
|
312
|
+
if (ms === 0)
|
|
313
|
+
return "—";
|
|
314
|
+
const diff = Date.now() - ms;
|
|
315
|
+
if (diff < 60_000)
|
|
316
|
+
return "just now";
|
|
317
|
+
const min = 60_000;
|
|
318
|
+
const hour = 60 * min;
|
|
319
|
+
const day = 24 * hour;
|
|
320
|
+
const week = 7 * day;
|
|
321
|
+
const month = 30 * day;
|
|
322
|
+
if (diff < hour)
|
|
323
|
+
return `${Math.floor(diff / min)}m ago`;
|
|
324
|
+
if (diff < day)
|
|
325
|
+
return `${Math.floor(diff / hour)}h ago`;
|
|
326
|
+
if (diff < week)
|
|
327
|
+
return `${Math.floor(diff / day)}d ago`;
|
|
328
|
+
if (diff < month)
|
|
329
|
+
return `${Math.floor(diff / week)}w ago`;
|
|
330
|
+
return `${Math.floor(diff / month)}mo ago`;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* List the most recent ezcoder sessions for a project cwd, newest first, each
|
|
334
|
+
* with a short preview built from its first user message. Used by the new-window
|
|
335
|
+
* project picker to offer "resume a session" alongside "new session".
|
|
336
|
+
*
|
|
337
|
+
* Fast path: instead of fully reading every session file in the project (what
|
|
338
|
+
* SessionManager.list does to count messages), sort files by mtime and read
|
|
339
|
+
* only the newest `limit`. Each chosen file is parsed in ONE pass for its
|
|
340
|
+
* header id, message count, last activity, and first user preview. For projects
|
|
341
|
+
* with many/large sessions this is the difference between scanning everything
|
|
342
|
+
* and scanning ~5 files.
|
|
343
|
+
*/
|
|
344
|
+
export async function listRecentSessions(cwd, limit = 5) {
|
|
345
|
+
const sessionsDir = getAppPaths().sessionsDir;
|
|
346
|
+
const dir = path.join(sessionsDir, encodeCwdForSessions(cwd));
|
|
347
|
+
const files = await collectJsonlFiles(dir, 1);
|
|
348
|
+
if (files.length === 0)
|
|
349
|
+
return [];
|
|
350
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
351
|
+
const out = [];
|
|
352
|
+
for (const f of files) {
|
|
353
|
+
if (out.length >= limit)
|
|
354
|
+
break;
|
|
355
|
+
const parsed = await readSessionSummary(f.path);
|
|
356
|
+
if (parsed && parsed.messageCount > 0)
|
|
357
|
+
out.push(parsed);
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Encode a cwd to its session directory name. Mirrors SessionManager's encoding
|
|
363
|
+
* (leading slash dropped, remaining slashes → underscores) so we resolve the
|
|
364
|
+
* same per-project folder without instantiating the manager.
|
|
365
|
+
*/
|
|
366
|
+
function encodeCwdForSessions(cwd) {
|
|
367
|
+
return cwd.replace(/[\\/]/g, "_").replace(/:/g, "").replace(/^_/, "");
|
|
368
|
+
}
|
|
369
|
+
/** Single-pass parse of one session file: header id + count + activity + preview. */
|
|
370
|
+
async function readSessionSummary(file) {
|
|
371
|
+
return new Promise((resolve) => {
|
|
372
|
+
const stream = createReadStream(file, { encoding: "utf-8" });
|
|
373
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
374
|
+
let id = "";
|
|
375
|
+
let messageCount = 0;
|
|
376
|
+
let lastActivity = "";
|
|
377
|
+
let preview = "";
|
|
378
|
+
let valid = false;
|
|
379
|
+
let done = false;
|
|
380
|
+
const finish = () => {
|
|
381
|
+
if (done)
|
|
382
|
+
return;
|
|
383
|
+
done = true;
|
|
384
|
+
resolve(valid
|
|
385
|
+
? { id, path: file, preview, lastActiveDisplay: rel(lastActivity), messageCount }
|
|
386
|
+
: null);
|
|
387
|
+
rl.close();
|
|
388
|
+
stream.destroy();
|
|
389
|
+
};
|
|
390
|
+
rl.on("line", (line) => {
|
|
391
|
+
if (done || !line)
|
|
392
|
+
return;
|
|
393
|
+
try {
|
|
394
|
+
const p = JSON.parse(line);
|
|
395
|
+
if (!valid) {
|
|
396
|
+
if (p.type !== "session")
|
|
397
|
+
return finish(); // not a session file
|
|
398
|
+
valid = true;
|
|
399
|
+
id = p.id ?? "";
|
|
400
|
+
if (p.timestamp)
|
|
401
|
+
lastActivity = p.timestamp;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (p.type === "message") {
|
|
405
|
+
messageCount++;
|
|
406
|
+
if (p.timestamp)
|
|
407
|
+
lastActivity = p.timestamp;
|
|
408
|
+
if (!preview && p.message?.role === "user") {
|
|
409
|
+
const text = extractText(p.message.content);
|
|
410
|
+
if (text)
|
|
411
|
+
preview = text.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// skip malformed line
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
rl.on("close", finish);
|
|
420
|
+
rl.on("error", finish);
|
|
421
|
+
stream.on("error", finish);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function rel(timestamp) {
|
|
425
|
+
return formatRelativeTime(Date.parse(timestamp) || 0);
|
|
426
|
+
}
|
|
427
|
+
function extractText(content) {
|
|
428
|
+
if (typeof content === "string")
|
|
429
|
+
return content;
|
|
430
|
+
if (Array.isArray(content)) {
|
|
431
|
+
for (const block of content) {
|
|
432
|
+
if (block && typeof block === "object" && "text" in block) {
|
|
433
|
+
const t = block.text;
|
|
434
|
+
if (typeof t === "string")
|
|
435
|
+
return t;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return "";
|
|
440
|
+
}
|
|
441
|
+
//# sourceMappingURL=project-discovery.js.map
|