@selesai/code 0.13.7 → 0.13.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/core/settings-manager.d.ts +11 -0
  3. package/dist/core/settings-manager.js +48 -0
  4. package/dist/extensions/auto-session-name.test.ts +13 -1
  5. package/dist/extensions/auto-session-name.ts +8 -4
  6. package/dist/extensions/ponytail/package.json +1 -1
  7. package/dist/modes/rpc/rpc-client.d.ts +130 -4
  8. package/dist/modes/rpc/rpc-client.js +156 -4
  9. package/dist/modes/rpc/rpc-mode.js +484 -1
  10. package/dist/modes/rpc/rpc-types.d.ts +249 -1
  11. package/dist/skills/unlazy/CHANGELOG.md +9 -0
  12. package/dist/skills/unlazy/CONTRIBUTING.md +6 -9
  13. package/dist/skills/unlazy/README.md +9 -23
  14. package/dist/skills/unlazy/SECURITY.md +7 -17
  15. package/dist/skills/unlazy/SKILL.md +4 -12
  16. package/dist/skills/unlazy/references/dispatch.md +14 -17
  17. package/dist/skills/unlazy/references/gates.md +3 -3
  18. package/dist/skills/unlazy/references/orchestration.md +0 -1
  19. package/dist/skills/unlazy/references/parallel.md +4 -16
  20. package/dist/skills/unlazy/references/token-economy.md +1 -1
  21. package/dist/skills/unlazy/research/validation-protocol.md +1 -1
  22. package/dist/skills/unlazy/scripts/gate-check.mjs +4 -19
  23. package/dist/skills/unlazy/scripts/gate-lint.mjs +1 -1
  24. package/dist/skills/unlazy/scripts/lib/gates.mjs +1 -5
  25. package/dist/skills/unlazy/templates/PLAN.md +1 -1
  26. package/dist/skills/unlazy/tests/dispatch-tests.mjs +9 -194
  27. package/dist/skills/unlazy/tests/hardening-tests.mjs +2 -5
  28. package/dist/skills/unlazy/tests/lint-tests.mjs +1 -1
  29. package/dist/skills/unlazy/tests/run-tests.mjs +1 -195
  30. package/dist/skills/unlazy/tests/self-check.mjs +0 -13
  31. package/dist/skills/unlazy/tests/stress-tests.mjs +0 -191
  32. package/docs/rpc.md +1 -1
  33. package/package.json +1 -1
  34. package/dist/skills/unlazy/scripts/install-hooks.mjs +0 -206
  35. package/dist/skills/unlazy/scripts/stop-hook.mjs +0 -174
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
+ ## [0.13.9] - 2026-09-01
6
+
7
+ ### Changed
8
+ - **Auto session naming only names unnamed sessions.** The bundled `auto-session-name` extension now skips naming entirely when the current session already has a name (user-set via `/name` or generated earlier), instead of re-naming on every user message. Previously generated names are never clobbered; empty sessions are still auto-named as before.
9
+
10
+ ## [0.13.8] - 2026-09-01
11
+
12
+ ### Changed
13
+ - **Unlazy skill is Selesai-native.** The bundled `unlazy` skill was scrubbed of its Claude Code / Codex assumptions. The Stop-hook installer, the `stop-hook` and `install-hooks` scripts, the `--bind` session binding, and the Codex/Claude launch adapters were removed; dispatch now runs on Selesai's native async `subagent` runs. The remaining hooks/installer references, hook and installer tests, and related documentation were removed.
14
+ - **Ponytail extension test script.** `src/extensions/ponytail` now uses `vitest run test/` as its test command, keeping the node builtin test run available as `test:node`.
15
+
5
16
  ## [0.13.7] - 2026-09-01
6
17
 
7
18
  ### Added
@@ -178,6 +178,17 @@ export declare class SettingsManager {
178
178
  private static migrateSettings;
179
179
  getGlobalSettings(): Settings;
180
180
  getProjectSettings(): Settings;
181
+ /** Merged effective settings (project overrides global, nested objects merge recursively). */
182
+ getEffectiveSettings(): Settings;
183
+ /**
184
+ * Bulk-update global settings and persist. Nested object fields merge
185
+ * recursively; undefined values are ignored (use null to clear a field).
186
+ */
187
+ setGlobalSettings(values: Partial<Settings>): void;
188
+ /**
189
+ * Bulk-update project settings and persist. Throws when the project is not trusted.
190
+ */
191
+ setProjectSettings(values: Partial<Settings>): void;
181
192
  isProjectTrusted(): boolean;
182
193
  setProjectTrusted(trusted: boolean): void;
183
194
  reload(): Promise<void>;
@@ -263,6 +263,54 @@ export class SettingsManager {
263
263
  getProjectSettings() {
264
264
  return structuredClone(this.projectSettings);
265
265
  }
266
+ /** Merged effective settings (project overrides global, nested objects merge recursively). */
267
+ getEffectiveSettings() {
268
+ return structuredClone(this.settings);
269
+ }
270
+ /**
271
+ * Bulk-update global settings and persist. Nested object fields merge
272
+ * recursively; undefined values are ignored (use null to clear a field).
273
+ */
274
+ setGlobalSettings(values) {
275
+ const merged = deepMergeSettings(this.globalSettings, values);
276
+ this.globalSettings = merged;
277
+ for (const field of Object.keys(values)) {
278
+ const value = values[field];
279
+ if (value === undefined)
280
+ continue;
281
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
282
+ for (const nestedKey of Object.keys(value)) {
283
+ this.markModified(field, nestedKey);
284
+ }
285
+ }
286
+ else {
287
+ this.markModified(field);
288
+ }
289
+ }
290
+ this.save();
291
+ }
292
+ /**
293
+ * Bulk-update project settings and persist. Throws when the project is not trusted.
294
+ */
295
+ setProjectSettings(values) {
296
+ this.assertProjectTrustedForWrite();
297
+ const projectSettings = deepMergeSettings(this.projectSettings, values);
298
+ this.projectSettings = projectSettings;
299
+ for (const field of Object.keys(values)) {
300
+ const value = values[field];
301
+ if (value === undefined)
302
+ continue;
303
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
304
+ for (const nestedKey of Object.keys(value)) {
305
+ this.markProjectModified(field, nestedKey);
306
+ }
307
+ }
308
+ else {
309
+ this.markProjectModified(field);
310
+ }
311
+ }
312
+ this.saveProjectSettings(this.projectSettings);
313
+ }
266
314
  isProjectTrusted() {
267
315
  return this.projectTrusted;
268
316
  }
@@ -35,9 +35,11 @@ function userEntry(text: string, id: string): SessionEntry {
35
35
  function createHarness(branch: SessionEntry[] = []) {
36
36
  const handlers = new Map<string, Handler>();
37
37
  const setSessionName = vi.fn();
38
+ const getSessionName = vi.fn(() => undefined);
38
39
  const pi = {
39
40
  on: vi.fn((event: string, handler: Handler) => handlers.set(event, handler)),
40
41
  setSessionName,
42
+ getSessionName,
41
43
  } as unknown as ExtensionAPI;
42
44
  const model = { provider: "openai", id: "gpt-5", name: "GPT-5" };
43
45
  const ctx = {
@@ -49,7 +51,7 @@ function createHarness(branch: SessionEntry[] = []) {
49
51
  } as unknown as ExtensionContext;
50
52
 
51
53
  autoSessionNameExtension(pi);
52
- return { handlers, setSessionName, ctx };
54
+ return { handlers, setSessionName, getSessionName, ctx };
53
55
  }
54
56
 
55
57
  function textResponse(text: string) {
@@ -70,6 +72,16 @@ describe("auto-session-name", () => {
70
72
  expect(handlers.has("input")).toBe(true);
71
73
  });
72
74
 
75
+ it("skips naming when the session already has a name", async () => {
76
+ const { handlers, getSessionName, setSessionName, ctx } = createHarness();
77
+ getSessionName.mockReturnValue("Existing name");
78
+
79
+ await handlers.get("input")!({ type: "input", text: "hi", source: "interactive" }, ctx);
80
+
81
+ expect(completeMock).not.toHaveBeenCalled();
82
+ expect(setSessionName).not.toHaveBeenCalled();
83
+ });
84
+
73
85
  it("names the session from the user prompt with a small system prompt and 10-token cap", async () => {
74
86
  const { handlers, setSessionName, ctx } = createHarness();
75
87
  completeMock.mockResolvedValue(textResponse("Refactor auth"));
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * auto-session-name
3
3
  *
4
- * Names the session on every user message using the currently selected model.
5
- * Sends only user messages (current prompt plus previous user messages,
6
- * truncated) for context, capped at 10 output tokens (1-5 word name).
7
- * Thinking is never enabled. Equivalent to running /name automatically.
4
+ * Names the session using the currently selected model, but only when the
5
+ * session has no name yet — a user-set or previously generated name is never
6
+ * clobbered. Sends only user messages (current prompt plus previous user
7
+ * messages, truncated) for context, capped at 10 output tokens (1-5 word
8
+ * name). Thinking is never enabled. Equivalent to running /name automatically.
8
9
  */
9
10
 
10
11
  import { complete } from "@earendil-works/pi-ai/compat";
@@ -52,6 +53,9 @@ export default function autoSessionNameExtension(pi: ExtensionAPI): void {
52
53
  const text = event.text.trim();
53
54
  if (!text || text.startsWith("/")) return;
54
55
  if (!ctx.model) return;
56
+ // Only auto-name sessions that have no name yet; never clobber a
57
+ // user-set or previously generated name.
58
+ if (pi.getSessionName()) return;
55
59
 
56
60
  const seq = ++namingSeq;
57
61
  void (async () => {
@@ -3,6 +3,6 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "scripts": {
6
- "test": "node --test ./test/*.test.js"
6
+ "test": "vitest run test/", "test:node": "node --test ./test/*.test.js"
7
7
  }
8
8
  }
@@ -4,11 +4,14 @@
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
6
  import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
7
- import type { ImageContent } from "@earendil-works/pi-ai";
7
+ import type { ImageContent, Model } from "@earendil-works/pi-ai";
8
8
  import type { AgentSessionEvent, SessionStats } from "../../core/agent-session.ts";
9
+ import type { KeybindingsConfig } from "../../core/keybindings.ts";
10
+ import type { Settings } from "../../core/settings-manager.ts";
9
11
  import type { BashResult } from "../../core/bash-executor.ts";
10
12
  import type { CompactionResult } from "../../core/compaction/index.ts";
11
13
  import type { SessionEntry, SessionTreeNode } from "../../core/session-manager.ts";
14
+ import type { ChangelogEntry } from "../../utils/changelog.ts";
12
15
  import type { RpcSessionState, RpcSlashCommand } from "./rpc-types.ts";
13
16
  export interface RpcClientOptions {
14
17
  /** Path to the CLI entry point (default: searches for dist/cli.js) */
@@ -217,9 +220,9 @@ export declare class RpcClient {
217
220
  leafId: string | null;
218
221
  }>;
219
222
  /**
220
- * Get the session entry tree.
223
+ * Get the session entry tree, optionally filtered.
221
224
  */
222
- getTree(): Promise<{
225
+ getTree(filter?: "default" | "no-tools" | "user-only" | "labeled-only" | "all"): Promise<{
223
226
  tree: SessionTreeNode[];
224
227
  leafId: string | null;
225
228
  }>;
@@ -236,9 +239,132 @@ export declare class RpcClient {
236
239
  */
237
240
  getMessages(): Promise<AgentMessage[]>;
238
241
  /**
239
- * Get available commands (extension commands, prompt templates).
242
+ * Get available commands (extension commands, prompt templates, builtins).
240
243
  */
241
244
  getCommands(): Promise<RpcSlashCommand[]>;
245
+ /**
246
+ * Get settings for a scope ("global" | "project" | "effective", default "effective").
247
+ */
248
+ getSettings(scope?: "global" | "project" | "effective"): Promise<{
249
+ scope: string;
250
+ settings: Settings;
251
+ }>;
252
+ /**
253
+ * Bulk-update settings and persist.
254
+ */
255
+ setSettings(values: Partial<Settings>, scope?: "global" | "project"): Promise<void>;
256
+ /**
257
+ * Reset settings to bundled factory defaults (backup preserved).
258
+ */
259
+ factoryResetSettings(): Promise<void>;
260
+ /**
261
+ * List providers and the login methods they support.
262
+ */
263
+ getAuthProviders(): Promise<Array<{
264
+ providerId: string;
265
+ name: string;
266
+ authTypes: Array<"oauth" | "api_key">;
267
+ }>>;
268
+ /**
269
+ * Login to a provider. API-key flows emit `extension_ui_request: input` with
270
+ * `inputKind: "secret"`; OAuth flows open a browser and emit notify events.
271
+ */
272
+ login(providerId: string, authType: "oauth" | "api_key"): Promise<{
273
+ providerId: string;
274
+ authType: "oauth" | "api_key";
275
+ message: string;
276
+ }>;
277
+ /**
278
+ * Log out of a provider, removing stored credentials.
279
+ */
280
+ logout(providerId: string): Promise<void>;
281
+ /**
282
+ * Get providers with configured auth (stored credential or environment).
283
+ */
284
+ getAuthState(): Promise<Array<{
285
+ providerId: string;
286
+ authType: "oauth" | "api_key";
287
+ source: string;
288
+ }>>;
289
+ /**
290
+ * Get enabled model patterns (Ctrl+P cycling scope) and available models.
291
+ */
292
+ getScopedModels(): Promise<{
293
+ enabled: string[];
294
+ models: Model<any>[];
295
+ }>;
296
+ /**
297
+ * Set enabled model patterns and/or reorder them (Ctrl+P cycling order).
298
+ */
299
+ setScopedModels(options?: {
300
+ enabled?: string[];
301
+ reorder?: string[];
302
+ }): Promise<string[]>;
303
+ /**
304
+ * Import and resume a session from a JSONL file (replaces current session).
305
+ */
306
+ importJsonl(path: string): Promise<string>;
307
+ /**
308
+ * Run the git helper (worktrees/checkpoints/commits/push). Same behavior as
309
+ * the `prompt` command: success is reported on preflight, then the normal
310
+ * agent/message event stream follows.
311
+ */
312
+ git(command: string): Promise<void>;
313
+ /**
314
+ * Reload keybindings, extensions, skills, prompts, themes, and context files.
315
+ */
316
+ reload(): Promise<void>;
317
+ /**
318
+ * Set or clear a label on a session entry (undefined/empty clears).
319
+ */
320
+ setEntryLabel(entryId: string, label?: string): Promise<string>;
321
+ /**
322
+ * Get the current project trust decision and status.
323
+ */
324
+ getTrust(): Promise<{
325
+ cwd: string;
326
+ savedDecision: {
327
+ path: string;
328
+ decision: boolean;
329
+ } | null;
330
+ projectTrusted: boolean;
331
+ trustRequired: boolean;
332
+ }>;
333
+ /**
334
+ * Save a project trust decision. Decision takes full effect after restart.
335
+ */
336
+ setTrust(decision: boolean | null): Promise<{
337
+ decision: boolean | null;
338
+ restartRequired: boolean;
339
+ }>;
340
+ /**
341
+ * Get the effective keybinding map.
342
+ */
343
+ getHotkeys(): Promise<KeybindingsConfig>;
344
+ /**
345
+ * Get available themes (built-in + custom) and the current theme setting.
346
+ */
347
+ getAvailableThemes(): Promise<{
348
+ themes: Array<{
349
+ name: string;
350
+ path?: string;
351
+ }>;
352
+ current: string | undefined;
353
+ }>;
354
+ /**
355
+ * Get the current version and parsed changelog entries.
356
+ */
357
+ getVersionInfo(): Promise<{
358
+ version: string;
359
+ changelog: ChangelogEntry[];
360
+ }>;
361
+ /**
362
+ * Share the current session as a secret GitHub gist (requires `gh` CLI).
363
+ */
364
+ shareGist(): Promise<{
365
+ url: string;
366
+ gistUrl: string;
367
+ }>;
242
368
  /**
243
369
  * Wait for agent to become idle (no streaming).
244
370
  * Resolves when agent_settled event is received.
@@ -332,10 +332,10 @@ export class RpcClient {
332
332
  return this.getData(response);
333
333
  }
334
334
  /**
335
- * Get the session entry tree.
335
+ * Get the session entry tree, optionally filtered.
336
336
  */
337
- async getTree() {
338
- const response = await this.send({ type: "get_tree" });
337
+ async getTree(filter) {
338
+ const response = await this.send({ type: "get_tree", ...(filter ? { filter } : {}) });
339
339
  return this.getData(response);
340
340
  }
341
341
  /**
@@ -359,13 +359,165 @@ export class RpcClient {
359
359
  return this.getData(response).messages;
360
360
  }
361
361
  /**
362
- * Get available commands (extension commands, prompt templates).
362
+ * Get available commands (extension commands, prompt templates, builtins).
363
363
  */
364
364
  async getCommands() {
365
365
  const response = await this.send({ type: "get_commands" });
366
366
  return this.getData(response).commands;
367
367
  }
368
368
  // =========================================================================
369
+ // Settings
370
+ // =========================================================================
371
+ /**
372
+ * Get settings for a scope ("global" | "project" | "effective", default "effective").
373
+ */
374
+ async getSettings(scope) {
375
+ const response = await this.send({ type: "get_settings", ...(scope ? { scope } : {}) });
376
+ return this.getData(response);
377
+ }
378
+ /**
379
+ * Bulk-update settings and persist.
380
+ */
381
+ async setSettings(values, scope = "global") {
382
+ await this.send({ type: "set_settings", scope, values });
383
+ }
384
+ /**
385
+ * Reset settings to bundled factory defaults (backup preserved).
386
+ */
387
+ async factoryResetSettings() {
388
+ await this.send({ type: "factory_reset_settings" });
389
+ }
390
+ // =========================================================================
391
+ // Auth
392
+ // =========================================================================
393
+ /**
394
+ * List providers and the login methods they support.
395
+ */
396
+ async getAuthProviders() {
397
+ const response = await this.send({ type: "get_auth_providers" });
398
+ return this.getData(response).providers;
399
+ }
400
+ /**
401
+ * Login to a provider. API-key flows emit `extension_ui_request: input` with
402
+ * `inputKind: "secret"`; OAuth flows open a browser and emit notify events.
403
+ */
404
+ async login(providerId, authType) {
405
+ const response = await this.send({ type: "login", providerId, authType });
406
+ return this.getData(response);
407
+ }
408
+ /**
409
+ * Log out of a provider, removing stored credentials.
410
+ */
411
+ async logout(providerId) {
412
+ await this.send({ type: "logout", providerId });
413
+ }
414
+ /**
415
+ * Get providers with configured auth (stored credential or environment).
416
+ */
417
+ async getAuthState() {
418
+ const response = await this.send({ type: "get_auth_state" });
419
+ return this.getData(response).providers;
420
+ }
421
+ // =========================================================================
422
+ // Scoped models
423
+ // =========================================================================
424
+ /**
425
+ * Get enabled model patterns (Ctrl+P cycling scope) and available models.
426
+ */
427
+ async getScopedModels() {
428
+ const response = await this.send({ type: "get_scoped_models" });
429
+ return this.getData(response);
430
+ }
431
+ /**
432
+ * Set enabled model patterns and/or reorder them (Ctrl+P cycling order).
433
+ */
434
+ async setScopedModels(options = {}) {
435
+ const response = await this.send({ type: "set_scoped_models", ...options });
436
+ return this.getData(response).enabled;
437
+ }
438
+ // =========================================================================
439
+ // Session import
440
+ // =========================================================================
441
+ /**
442
+ * Import and resume a session from a JSONL file (replaces current session).
443
+ */
444
+ async importJsonl(path) {
445
+ const response = await this.send({ type: "import_jsonl", path });
446
+ return this.getData(response).sessionPath;
447
+ }
448
+ // =========================================================================
449
+ // Git helper / reload / tree ops
450
+ // =========================================================================
451
+ /**
452
+ * Run the git helper (worktrees/checkpoints/commits/push). Same behavior as
453
+ * the `prompt` command: success is reported on preflight, then the normal
454
+ * agent/message event stream follows.
455
+ */
456
+ async git(command) {
457
+ await this.send({ type: "git", command });
458
+ }
459
+ /**
460
+ * Reload keybindings, extensions, skills, prompts, themes, and context files.
461
+ */
462
+ async reload() {
463
+ await this.send({ type: "reload" });
464
+ }
465
+ /**
466
+ * Set or clear a label on a session entry (undefined/empty clears).
467
+ */
468
+ async setEntryLabel(entryId, label) {
469
+ const response = await this.send({ type: "set_entry_label", entryId, label });
470
+ return this.getData(response).labelId;
471
+ }
472
+ // =========================================================================
473
+ // Trust
474
+ // =========================================================================
475
+ /**
476
+ * Get the current project trust decision and status.
477
+ */
478
+ async getTrust() {
479
+ const response = await this.send({ type: "get_trust" });
480
+ return this.getData(response);
481
+ }
482
+ /**
483
+ * Save a project trust decision. Decision takes full effect after restart.
484
+ */
485
+ async setTrust(decision) {
486
+ const response = await this.send({ type: "set_trust", decision });
487
+ return this.getData(response);
488
+ }
489
+ // =========================================================================
490
+ // Hotkeys / version / share
491
+ // =========================================================================
492
+ /**
493
+ * Get the effective keybinding map.
494
+ */
495
+ async getHotkeys() {
496
+ const response = await this.send({ type: "get_hotkeys" });
497
+ return this.getData(response).hotkeys;
498
+ }
499
+ /**
500
+ * Get available themes (built-in + custom) and the current theme setting.
501
+ */
502
+ async getAvailableThemes() {
503
+ const response = await this.send({ type: "get_available_themes" });
504
+ return this.getData(response);
505
+ }
506
+ /**
507
+ * Get the current version and parsed changelog entries.
508
+ */
509
+ async getVersionInfo() {
510
+ const response = await this.send({ type: "get_version_info" });
511
+ return this.getData(response);
512
+ }
513
+ /**
514
+ * Share the current session as a secret GitHub gist (requires `gh` CLI).
515
+ */
516
+ async shareGist() {
517
+ const response = await this.send({ type: "share_gist", public: false });
518
+ return this.getData(response);
519
+ }
520
+ // =========================================================================
369
521
  // Helpers
370
522
  // =========================================================================
371
523
  /**