@x-code-cli/core 0.1.10 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/api-errors.d.ts.map +1 -1
- package/dist/agent/api-errors.js +18 -0
- package/dist/agent/api-errors.js.map +1 -1
- package/dist/agent/diff.d.ts +35 -0
- package/dist/agent/diff.d.ts.map +1 -0
- package/dist/agent/diff.js +83 -0
- package/dist/agent/diff.js.map +1 -0
- package/dist/agent/loop-state.d.ts +45 -3
- package/dist/agent/loop-state.d.ts.map +1 -1
- package/dist/agent/loop-state.js +24 -3
- package/dist/agent/loop-state.js.map +1 -1
- package/dist/agent/loop.d.ts +10 -6
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +212 -30
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/plan-storage.d.ts +55 -0
- package/dist/agent/plan-storage.d.ts.map +1 -0
- package/dist/agent/plan-storage.js +156 -0
- package/dist/agent/plan-storage.js.map +1 -0
- package/dist/agent/session-store.d.ts +114 -0
- package/dist/agent/session-store.d.ts.map +1 -0
- package/dist/agent/session-store.js +415 -0
- package/dist/agent/session-store.js.map +1 -0
- package/dist/agent/sub-agents/built-in.d.ts +3 -0
- package/dist/agent/sub-agents/built-in.d.ts.map +1 -0
- package/dist/agent/sub-agents/built-in.js +98 -0
- package/dist/agent/sub-agents/built-in.js.map +1 -0
- package/dist/agent/sub-agents/index.d.ts +7 -0
- package/dist/agent/sub-agents/index.d.ts.map +1 -0
- package/dist/agent/sub-agents/index.js +5 -0
- package/dist/agent/sub-agents/index.js.map +1 -0
- package/dist/agent/sub-agents/loader.d.ts +5 -0
- package/dist/agent/sub-agents/loader.d.ts.map +1 -0
- package/dist/agent/sub-agents/loader.js +117 -0
- package/dist/agent/sub-agents/loader.js.map +1 -0
- package/dist/agent/sub-agents/registry.d.ts +14 -0
- package/dist/agent/sub-agents/registry.d.ts.map +1 -0
- package/dist/agent/sub-agents/registry.js +37 -0
- package/dist/agent/sub-agents/registry.js.map +1 -0
- package/dist/agent/sub-agents/runner.d.ts +26 -0
- package/dist/agent/sub-agents/runner.d.ts.map +1 -0
- package/dist/agent/sub-agents/runner.js +287 -0
- package/dist/agent/sub-agents/runner.js.map +1 -0
- package/dist/agent/sub-agents/types.d.ts +63 -0
- package/dist/agent/sub-agents/types.d.ts.map +1 -0
- package/dist/agent/sub-agents/types.js +2 -0
- package/dist/agent/sub-agents/types.js.map +1 -0
- package/dist/agent/system-prompt.d.ts +15 -0
- package/dist/agent/system-prompt.d.ts.map +1 -1
- package/dist/agent/system-prompt.js +161 -0
- package/dist/agent/system-prompt.js.map +1 -1
- package/dist/agent/tool-execution.d.ts +4 -3
- package/dist/agent/tool-execution.d.ts.map +1 -1
- package/dist/agent/tool-execution.js +316 -14
- package/dist/agent/tool-execution.js.map +1 -1
- package/dist/agent/tool-result-sanitize.d.ts +12 -0
- package/dist/agent/tool-result-sanitize.d.ts.map +1 -1
- package/dist/agent/tool-result-sanitize.js +70 -0
- package/dist/agent/tool-result-sanitize.js.map +1 -1
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/knowledge/session.d.ts +4 -7
- package/dist/knowledge/session.d.ts.map +1 -1
- package/dist/knowledge/session.js +20 -55
- package/dist/knowledge/session.js.map +1 -1
- package/dist/permissions/index.d.ts +18 -3
- package/dist/permissions/index.d.ts.map +1 -1
- package/dist/permissions/index.js +20 -2
- package/dist/permissions/index.js.map +1 -1
- package/dist/tools/ask-user.d.ts.map +1 -1
- package/dist/tools/ask-user.js +8 -6
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/enter-plan-mode.d.ts +25 -0
- package/dist/tools/enter-plan-mode.d.ts.map +1 -0
- package/dist/tools/enter-plan-mode.js +120 -0
- package/dist/tools/enter-plan-mode.js.map +1 -0
- package/dist/tools/exit-plan-mode.d.ts +13 -0
- package/dist/tools/exit-plan-mode.d.ts.map +1 -0
- package/dist/tools/exit-plan-mode.js +22 -0
- package/dist/tools/exit-plan-mode.js.map +1 -0
- package/dist/tools/grep.d.ts +1 -1
- package/dist/tools/index.d.ts +20 -4
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +7 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/save-knowledge.d.ts +2 -2
- package/dist/tools/shell-provider.d.ts +4 -0
- package/dist/tools/shell-provider.d.ts.map +1 -1
- package/dist/tools/shell-provider.js +2 -0
- package/dist/tools/shell-provider.js.map +1 -1
- package/dist/tools/task.d.ts +14 -0
- package/dist/tools/task.d.ts.map +1 -0
- package/dist/tools/task.js +95 -0
- package/dist/tools/task.js.map +1 -0
- package/dist/tools/todo-write.d.ts +21 -0
- package/dist/tools/todo-write.d.ts.map +1 -0
- package/dist/tools/todo-write.js +117 -0
- package/dist/tools/todo-write.js.map +1 -0
- package/dist/types/index.d.ts +103 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/knowledge/session-usage.d.ts +0 -24
- package/dist/knowledge/session-usage.d.ts.map +0 -1
- package/dist/knowledge/session-usage.js +0 -86
- package/dist/knowledge/session-usage.js.map +0 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// @x-code-cli/core — Plan-mode file storage
|
|
2
|
+
//
|
|
3
|
+
// Plans live in `.x-code/plans/<slug>-<YYYYMMDD-HHMMSS>.md` inside the
|
|
4
|
+
// user's project (NOT in the global `~/.x-code/`). This mirrors how
|
|
5
|
+
// `.x-code/sessions/` and `.x-code/memory/` are scoped: per-project,
|
|
6
|
+
// gitignored, never shared across repos. The slug-then-timestamp shape
|
|
7
|
+
// matches the legacy filenames already living under `.x-code/plans/`
|
|
8
|
+
// (e.g. `vue-3-vite-typescript-snake-game-20260420-102410.md`) — both
|
|
9
|
+
// human-skimmable in `ls` AND sortable by recency.
|
|
10
|
+
//
|
|
11
|
+
// Claude Code stores plans globally under `~/.claude/plans/{slug}.md`
|
|
12
|
+
// with random word-pair slugs (`brilliant-crystal.md`). We chose
|
|
13
|
+
// project-local + topic-derived slug on the user's request — easier to
|
|
14
|
+
// find later, and the plan stays with the repo it was written for.
|
|
15
|
+
import fs from 'node:fs/promises';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { generateText } from 'ai';
|
|
18
|
+
import { getThinkingProviderOptions } from '../providers/thinking.js';
|
|
19
|
+
import { debugLog, XCODE_DIR } from '../utils.js';
|
|
20
|
+
const PLANS_SUBDIR = 'plans';
|
|
21
|
+
const SLUG_MAX_LEN = 40;
|
|
22
|
+
/** Convert an arbitrary task description into a filesystem-safe,
|
|
23
|
+
* lower-case, hyphen-separated slug. Drops anything outside
|
|
24
|
+
* `[a-z0-9 -]` (so CJK / emoji / punctuation collapse to nothing —
|
|
25
|
+
* CJK-only tasks produce an empty slug, which is intentional and
|
|
26
|
+
* caught by callers' timestamp-only fallback). Length capped at
|
|
27
|
+
* SLUG_MAX_LEN cells so `ls` columns stay readable. Exported so
|
|
28
|
+
* session-usage filenames can mirror the same shape as plan files. */
|
|
29
|
+
export function slugify(text) {
|
|
30
|
+
return text
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.trim()
|
|
33
|
+
.replace(/[^a-z0-9\s-]+/g, '')
|
|
34
|
+
.replace(/\s+/g, '-')
|
|
35
|
+
.replace(/-+/g, '-')
|
|
36
|
+
.replace(/^-+|-+$/g, '')
|
|
37
|
+
.slice(0, SLUG_MAX_LEN)
|
|
38
|
+
.replace(/-+$/g, '');
|
|
39
|
+
}
|
|
40
|
+
/** Format a Date as `YYYYMMDD-HHMMSS`. Local time, no zone suffix —
|
|
41
|
+
* matches the legacy plan-file convention which is what the user is
|
|
42
|
+
* used to scanning visually. */
|
|
43
|
+
function formatTimestamp(d) {
|
|
44
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
45
|
+
return (`${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}` +
|
|
46
|
+
`-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`);
|
|
47
|
+
}
|
|
48
|
+
function plansDir() {
|
|
49
|
+
return path.join(process.cwd(), XCODE_DIR, PLANS_SUBDIR);
|
|
50
|
+
}
|
|
51
|
+
/** Build a fresh plan-file path from a task description (typically the
|
|
52
|
+
* user's most recent message). Pure function — no I/O — so callers
|
|
53
|
+
* can stash the path on LoopState before the file actually exists.
|
|
54
|
+
* Format: `<slug>-<timestamp>.md` (slug-only when timestamp could
|
|
55
|
+
* conflict, timestamp-only when the task text produces an empty
|
|
56
|
+
* slug). Pass `opts.slug` when the caller already has a precomputed
|
|
57
|
+
* slug (e.g. agentLoop's session-wide LLM-generated `taskSlug`) to
|
|
58
|
+
* skip the local slugify pass — important for non-ASCII task text
|
|
59
|
+
* where slugify would return empty. */
|
|
60
|
+
export function makePlanFilePath(taskText, opts) {
|
|
61
|
+
const slug = opts?.slug ?? slugify(taskText);
|
|
62
|
+
const ts = formatTimestamp(opts?.now ?? new Date());
|
|
63
|
+
const name = slug ? `${slug}-${ts}` : ts;
|
|
64
|
+
return path.join(plansDir(), `${name}.md`);
|
|
65
|
+
}
|
|
66
|
+
/** Min length of a locally-slugified result for the fast path to
|
|
67
|
+
* apply. Below this we assume the user's first message had little
|
|
68
|
+
* ASCII content (typical CJK-only message: 0; "fix bug": ≥6) and ask
|
|
69
|
+
* the model for an English summary instead of producing an unhelpful
|
|
70
|
+
* one-letter filename. */
|
|
71
|
+
const ASCII_FAST_PATH_MIN_LEN = 6;
|
|
72
|
+
/** Cap on raw user text sent to the slug model. The summary only
|
|
73
|
+
* needs the gist; a 5000-character paste would just waste input
|
|
74
|
+
* tokens. */
|
|
75
|
+
const TASK_TEXT_TRUNCATE = 500;
|
|
76
|
+
/** Hard cap on output tokens for the slug call. Sized for "2-4 short
|
|
77
|
+
* English words" (~10 visible tokens) PLUS a comfortable margin for
|
|
78
|
+
* reasoning models that emit hidden thinking tokens before any
|
|
79
|
+
* visible text. We disable thinking explicitly below where the
|
|
80
|
+
* provider supports it, but DeepSeek's `disabled` and Anthropic's
|
|
81
|
+
* `disabled` aren't always honored on every model id, so the budget
|
|
82
|
+
* has to survive a small amount of forced reasoning too. */
|
|
83
|
+
const SLUG_MAX_OUTPUT_TOKENS = 256;
|
|
84
|
+
/** Derive a human-skimmable filename slug for the session.
|
|
85
|
+
*
|
|
86
|
+
* Fast path: if `slugify(taskText)` already produces ≥6 chars (i.e.
|
|
87
|
+
* the user typed something English-y), return it directly — zero
|
|
88
|
+
* network, zero tokens. Covers the entire English-prompt user base.
|
|
89
|
+
*
|
|
90
|
+
* Slow path: for CJK-only / emoji-heavy / very short first messages
|
|
91
|
+
* where slugify returns empty or near-empty, make ONE isolated
|
|
92
|
+
* generateText call asking for 2-4 lowercase English words. No
|
|
93
|
+
* message history, no tools, no system context — just the user's
|
|
94
|
+
* raw text (truncated) and a strict instruction. Disables thinking
|
|
95
|
+
* on providers that support it so the small token budget isn't
|
|
96
|
+
* spent on hidden reasoning before any visible text appears.
|
|
97
|
+
*
|
|
98
|
+
* Returns '' on any failure (including abort). Callers treat empty
|
|
99
|
+
* as "fall back to timestamp-only naming", matching pre-existing
|
|
100
|
+
* behavior so adding this helper can't regress anyone. */
|
|
101
|
+
export async function generateTaskSlug(taskText, model, modelId, signal) {
|
|
102
|
+
const localSlug = slugify(taskText);
|
|
103
|
+
if (localSlug.length >= ASCII_FAST_PATH_MIN_LEN) {
|
|
104
|
+
debugLog('slug.fast-path', `len=${localSlug.length} slug="${localSlug}"`);
|
|
105
|
+
return localSlug;
|
|
106
|
+
}
|
|
107
|
+
debugLog('slug.llm-start', `taskTextLen=${taskText.length} modelId=${modelId}`);
|
|
108
|
+
try {
|
|
109
|
+
const { text, usage, finishReason } = await generateText({
|
|
110
|
+
model,
|
|
111
|
+
abortSignal: signal,
|
|
112
|
+
providerOptions: getThinkingProviderOptions(modelId, false),
|
|
113
|
+
system: 'You convert user task descriptions into short English filename slugs. ' +
|
|
114
|
+
'Reply with ONLY 2 to 4 lowercase English words separated by spaces. ' +
|
|
115
|
+
'No punctuation, no quotes, no explanation, no prefixes like "slug:". ' +
|
|
116
|
+
'If the input is non-English, translate the gist into English first.',
|
|
117
|
+
prompt: taskText.slice(0, TASK_TEXT_TRUNCATE),
|
|
118
|
+
maxOutputTokens: SLUG_MAX_OUTPUT_TOKENS,
|
|
119
|
+
});
|
|
120
|
+
const slug = slugify(text);
|
|
121
|
+
debugLog('slug.llm-result', `finishReason=${finishReason} rawText="${(text ?? '').slice(0, 80)}" slug="${slug}" tokens=${usage?.outputTokens ?? '?'}`);
|
|
122
|
+
return slug;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
debugLog('slug.llm-error', err instanceof Error ? err.message : String(err));
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Make sure the plan directory exists. Recursive mkdir so we don't have
|
|
130
|
+
* to also ensure `.x-code/` separately — first plan written in a fresh
|
|
131
|
+
* project gets the parent created automatically. */
|
|
132
|
+
export async function ensurePlanDir() {
|
|
133
|
+
await fs.mkdir(plansDir(), { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
/** Read the plan body at `planPath`. Empty string when the file doesn't
|
|
136
|
+
* exist — exitPlanMode calls this to grab whatever the model has
|
|
137
|
+
* written so far, and "no plan written yet" is a valid (if unhelpful)
|
|
138
|
+
* state. */
|
|
139
|
+
export async function readPlan(planPath) {
|
|
140
|
+
try {
|
|
141
|
+
return await fs.readFile(planPath, 'utf-8');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Persist the plan body to `planPath`. Used by exitPlanMode when the
|
|
148
|
+
* model passes a `plan` override so the on-disk record matches what
|
|
149
|
+
* the user is approving. Returns the path it wrote to (always equal
|
|
150
|
+
* to the input). */
|
|
151
|
+
export async function writePlan(planPath, body) {
|
|
152
|
+
await ensurePlanDir();
|
|
153
|
+
await fs.writeFile(planPath, body, 'utf-8');
|
|
154
|
+
return planPath;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=plan-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-storage.js","sourceRoot":"","sources":["../../src/agent/plan-storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ModelMessage } from 'ai';
|
|
2
|
+
import type { PermissionMode, TokenUsage } from '../types/index.js';
|
|
3
|
+
import type { LoopState } from './loop-state.js';
|
|
4
|
+
/** Build the on-disk filename for a session. Same shape as plan files
|
|
5
|
+
* (`<slug>-<id>.<ext>`) so `ls .x-code/sessions/` and `ls .x-code/plans/`
|
|
6
|
+
* scan the same way. Empty slug (CJK-only first message) collapses to
|
|
7
|
+
* pure-timestamp naming, matching the plan-file fallback. */
|
|
8
|
+
export declare function getSessionFilePath(state: {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
taskSlug: string;
|
|
11
|
+
}, cwd?: string): string;
|
|
12
|
+
/** Write the session header. Idempotent: if the file already exists (resume
|
|
13
|
+
* path), we skip — the original header is preserved so picker metadata
|
|
14
|
+
* stays stable across resumes. */
|
|
15
|
+
export declare function appendHeader(state: LoopState, modelId: string, firstPrompt: string, cwd?: string): Promise<void>;
|
|
16
|
+
/** Flush every message in `state.messages` past `state.persistedMessageCount`
|
|
17
|
+
* to the jsonl file. The diff-based design keeps the writer decoupled from
|
|
18
|
+
* the many places in the agent loop that mutate state.messages directly
|
|
19
|
+
* (collectTurnResponse, processToolCalls, length-finish nudge, etc.) — we
|
|
20
|
+
* catch them all by sweeping at turn boundaries.
|
|
21
|
+
*
|
|
22
|
+
* After deep / light compaction the in-memory array shrinks. Callers must
|
|
23
|
+
* call `markBoundaryAndReflush` (below) instead of this — that path writes
|
|
24
|
+
* a compact-boundary marker so the loader can correctly truncate-on-load
|
|
25
|
+
* and then re-appends the trimmed messages so post-boundary jsonl content
|
|
26
|
+
* matches the new in-memory state. */
|
|
27
|
+
export declare function flushPendingMessages(state: LoopState): Promise<void>;
|
|
28
|
+
/** Append a usage snapshot for the current turn. Called from the agent loop
|
|
29
|
+
* after `collectTurnResponse` accepts the provider's `usage` object. The
|
|
30
|
+
* picker reads only the LAST usage line (tail scan) to display per-session
|
|
31
|
+
* totals — no need to keep older snapshots around any more efficiently. */
|
|
32
|
+
export declare function appendUsage(state: LoopState, modelId: string): Promise<void>;
|
|
33
|
+
/** Mark a compaction event and re-flush the (just-shrunk) message array.
|
|
34
|
+
* After this returns, the jsonl post-last-boundary content equals
|
|
35
|
+
* `state.messages` exactly — `loadSession` reconstructs the same in-memory
|
|
36
|
+
* state on resume.
|
|
37
|
+
*
|
|
38
|
+
* Why we re-append instead of relying on the pre-boundary messages: our
|
|
39
|
+
* `compressMessages` keeps a `recent N` slice verbatim, but those slices
|
|
40
|
+
* were already persisted before the boundary; the loader's
|
|
41
|
+
* "everything-after-last-boundary wins" rule would otherwise drop them.
|
|
42
|
+
* Duplicating ~6 messages on disk is cheap and keeps the load logic
|
|
43
|
+
* trivial.
|
|
44
|
+
*
|
|
45
|
+
* Light compaction (loop-guard pruning) calls this with `summary=undefined`
|
|
46
|
+
* — the trimmed messages still need a boundary so the loader doesn't
|
|
47
|
+
* resurrect the dropped loop-guard pairs. */
|
|
48
|
+
export declare function markBoundaryAndReflush(state: LoopState, summary?: string): Promise<void>;
|
|
49
|
+
/** Append an `interrupted` marker. Purely informational — the loader
|
|
50
|
+
* ignores it for state reconstruction; the picker can show "interrupted"
|
|
51
|
+
* next to sessions that ended mid-turn so users know what they're
|
|
52
|
+
* resuming into. */
|
|
53
|
+
export declare function appendInterrupted(state: LoopState): Promise<void>;
|
|
54
|
+
export interface LoadedSession {
|
|
55
|
+
sessionId: string;
|
|
56
|
+
taskSlug: string;
|
|
57
|
+
startedAt: string;
|
|
58
|
+
modelId: string;
|
|
59
|
+
cwd: string;
|
|
60
|
+
gitBranch?: string;
|
|
61
|
+
firstPrompt: string;
|
|
62
|
+
messages: ModelMessage[];
|
|
63
|
+
tokenUsage: TokenUsage;
|
|
64
|
+
turnCount: number;
|
|
65
|
+
/** Path of the jsonl file so the agent loop can keep appending to the
|
|
66
|
+
* same file when the user resumes. */
|
|
67
|
+
filePath: string;
|
|
68
|
+
}
|
|
69
|
+
/** Walk a session jsonl and reconstruct a LoadedSession.
|
|
70
|
+
*
|
|
71
|
+
* Compact-boundary semantics (matches Claude Code): every time we see a
|
|
72
|
+
* `compact-boundary` line, the message accumulator is cleared. So the
|
|
73
|
+
* returned `messages` reflects only what's after the LAST boundary —
|
|
74
|
+
* which by construction equals the in-memory state at the point of
|
|
75
|
+
* compaction (see `markBoundaryAndReflush`).
|
|
76
|
+
*
|
|
77
|
+
* Trailing tool_call / tool_result orphans are trimmed (the next API
|
|
78
|
+
* request would otherwise reject the message array) — see
|
|
79
|
+
* `sanitizeMessageTail` for the exact rule. */
|
|
80
|
+
export declare function loadSession(filePath: string): Promise<LoadedSession | null>;
|
|
81
|
+
export interface SessionListEntry {
|
|
82
|
+
filePath: string;
|
|
83
|
+
sessionId: string;
|
|
84
|
+
taskSlug: string;
|
|
85
|
+
firstPrompt: string;
|
|
86
|
+
startedAt: string;
|
|
87
|
+
modelId: string;
|
|
88
|
+
/** File mtime in epoch milliseconds — sort key for the picker. */
|
|
89
|
+
mtime: number;
|
|
90
|
+
tokenUsage: TokenUsage | null;
|
|
91
|
+
}
|
|
92
|
+
/** Enumerate every session jsonl in the current project, newest first.
|
|
93
|
+
* Reads only the head (~8KB, for the header line) and tail (~4KB, for
|
|
94
|
+
* the last usage line) of each file — no full-file load — so the picker
|
|
95
|
+
* is responsive even with hundreds of historical sessions. Files
|
|
96
|
+
* without a parseable header are dropped silently. */
|
|
97
|
+
export declare function listSessions(cwd?: string): Promise<SessionListEntry[]>;
|
|
98
|
+
/** Pick the most recently modified session file in the current project, or
|
|
99
|
+
* null if none exist. Used by `xc --continue` / `-c` to skip the picker
|
|
100
|
+
* and resume the latest session unconditionally. */
|
|
101
|
+
export declare function pickLatestSession(cwd?: string): Promise<SessionListEntry | null>;
|
|
102
|
+
/** Stable identifier for a session in picker UI. We can't use the filename
|
|
103
|
+
* directly (it can collide visually when multiple sessions share a slug)
|
|
104
|
+
* and the sessionId alone isn't unique across renames — but the file path
|
|
105
|
+
* is, by definition. Hashed to keep the choice label compact. */
|
|
106
|
+
export declare function shortIdFor(filePath: string): string;
|
|
107
|
+
/** Build a LoopState seeded from a previously-saved session. The agent
|
|
108
|
+
* loop accepts `existingState` and will continue appending to the same
|
|
109
|
+
* jsonl file (filename derives from `sessionId` + `taskSlug`, both
|
|
110
|
+
* preserved here). `persistedMessageCount` is set to the loaded length
|
|
111
|
+
* so the very first flush after the next user submit only appends NEW
|
|
112
|
+
* messages — the loaded tail is already on disk. */
|
|
113
|
+
export declare function hydrateLoopState(loaded: LoadedSession, initialMode?: PermissionMode): LoopState;
|
|
114
|
+
//# sourceMappingURL=session-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../src/agent/session-store.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AAEtC,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAGnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAQhD;;;8DAG8D;AAC9D,wBAAgB,kBAAkB,CAChC,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAC9C,GAAG,GAAE,MAAsB,GAC1B,MAAM,CAGR;AA6ED;;mCAEmC;AACnC,wBAAsB,YAAY,CAChC,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;;;;;;;;;uCAUuC;AACvC,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB1E;AAED;;;4EAG4E;AAC5E,wBAAsB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWlF;AAED;;;;;;;;;;;;;;8CAc8C;AAC9C,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED;;;qBAGqB;AACrB,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvE;AAID,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB;2CACuC;IACvC,QAAQ,EAAE,MAAM,CAAA;CACjB;AAWD;;;;;;;;;;gDAUgD;AAChD,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA+CjF;AAqDD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,UAAU,GAAG,IAAI,CAAA;CAC9B;AAED;;;;uDAIuD;AACvD,wBAAsB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuD3F;AAgBD;;qDAEqD;AACrD,wBAAsB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAGrG;AAED;;;kEAGkE;AAClE,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;qDAKqD;AACrD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,WAAW,GAAE,cAA0B,GAAG,SAAS,CAW1G"}
|