sfora-cli 0.15.0 → 0.17.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.
Files changed (94) hide show
  1. package/README.md +80 -1
  2. package/dist/agent-webhook.d.ts +20 -0
  3. package/dist/agent-webhook.js +42 -0
  4. package/dist/api-client.d.ts +97 -0
  5. package/dist/api-client.js +68 -0
  6. package/dist/ask.d.ts +51 -0
  7. package/dist/ask.js +70 -0
  8. package/dist/attachments-node.d.ts +7 -0
  9. package/dist/attachments-node.js +15 -0
  10. package/dist/attachments.d.ts +112 -0
  11. package/dist/attachments.js +254 -0
  12. package/dist/block-commands.d.ts +10 -0
  13. package/dist/block-commands.js +28 -0
  14. package/dist/chat.d.ts +15 -0
  15. package/dist/chat.js +7 -0
  16. package/dist/cli-args.d.ts +7 -0
  17. package/dist/cli-args.js +28 -1
  18. package/dist/cli.d.ts +12 -1
  19. package/dist/cli.js +186 -19
  20. package/dist/format/linkUrls.d.ts +2 -0
  21. package/dist/format/linkUrls.js +48 -0
  22. package/dist/format/postMarkdown.d.ts +12 -1
  23. package/dist/format/postMarkdown.js +9 -2
  24. package/dist/index.d.ts +23 -1
  25. package/dist/index.js +17 -1
  26. package/dist/local-core/files.d.ts +1 -1
  27. package/dist/local-core/files.js +2 -2
  28. package/dist/local-core/index.d.ts +12 -0
  29. package/dist/local-core/index.js +11 -0
  30. package/dist/local-core/skill-adapters.d.ts +21 -0
  31. package/dist/local-core/skill-adapters.js +19 -0
  32. package/dist/local-core/skill-discovery.d.ts +22 -0
  33. package/dist/local-core/skill-discovery.js +79 -0
  34. package/dist/local-core/skill-domain.d.ts +74 -0
  35. package/dist/local-core/skill-domain.js +1 -0
  36. package/dist/local-core/skill-executor.d.ts +23 -0
  37. package/dist/local-core/skill-executor.js +51 -0
  38. package/dist/local-core/skill-index.d.ts +54 -0
  39. package/dist/local-core/skill-index.js +115 -0
  40. package/dist/local-core/skill-local-executor.d.ts +18 -0
  41. package/dist/local-core/skill-local-executor.js +249 -0
  42. package/dist/local-core/skill-operations.d.ts +61 -0
  43. package/dist/local-core/skill-operations.js +268 -0
  44. package/dist/local-core/skill-review.d.ts +46 -0
  45. package/dist/local-core/skill-review.js +132 -0
  46. package/dist/local-core/skill-service.d.ts +96 -0
  47. package/dist/local-core/skill-service.js +157 -0
  48. package/dist/local-core/skill-store.d.ts +34 -0
  49. package/dist/local-core/skill-store.js +187 -0
  50. package/dist/local-core/skill-sync.d.ts +132 -0
  51. package/dist/local-core/skill-sync.js +111 -0
  52. package/dist/local-core/skills.d.ts +35 -0
  53. package/dist/local-core/skills.js +142 -37
  54. package/dist/mcp-description.d.ts +11 -0
  55. package/dist/mcp-description.js +29 -0
  56. package/dist/mcp-server.d.ts +5 -1
  57. package/dist/mcp-server.js +28 -18
  58. package/dist/shell-commands.d.ts +7 -1
  59. package/dist/shell-commands.js +49 -3
  60. package/dist/skills-client.d.ts +13 -2
  61. package/dist/skills-client.js +57 -5
  62. package/dist/skills-command.d.ts +1 -1
  63. package/dist/skills-command.js +171 -4
  64. package/dist/skills-packet/sfora-asks/SKILL.md +49 -0
  65. package/dist/skills-packet/sfora-asks/references/asks.md +41 -0
  66. package/dist/skills-packet/sfora-board/SKILL.md +53 -0
  67. package/dist/skills-packet/sfora-board/references/board.md +60 -0
  68. package/dist/skills-packet/sfora-board/references/plan.md +25 -0
  69. package/dist/skills-packet/sfora-chat/SKILL.md +55 -0
  70. package/dist/skills-packet/sfora-chat/references/rooms.md +45 -0
  71. package/dist/skills-packet/sfora-chat/references/waiting.md +37 -0
  72. package/dist/skills-packet/sfora-live-edit/SKILL.md +62 -0
  73. package/dist/skills-packet/sfora-live-edit/references/collisions.md +54 -0
  74. package/dist/skills-packet/sfora-live-edit/references/http.md +63 -0
  75. package/dist/skills-packet/sfora-live-edit/references/live-editing.md +49 -0
  76. package/dist/skills-packet/sfora-setup/SKILL.md +37 -0
  77. package/dist/skills-packet/sfora-setup/references/sign-in.md +43 -0
  78. package/dist/skills-packet/sfora-skills/SKILL.md +54 -0
  79. package/dist/skills-packet/sfora-skills/references/skills.md +78 -0
  80. package/dist/skills-packet/sfora-troubleshoot/SKILL.md +39 -0
  81. package/dist/skills-packet/sfora-troubleshoot/references/sharp-edges.md +80 -0
  82. package/dist/skills-packet/sfora-write/SKILL.md +53 -0
  83. package/dist/skills-packet/sfora-write/references/attachments.md +15 -0
  84. package/dist/skills-packet/sfora-write/references/blocks.md +36 -0
  85. package/dist/skills-packet/sfora-write/references/posts-and-docs.md +56 -0
  86. package/dist/skills-packet.d.ts +63 -0
  87. package/dist/skills-packet.js +166 -0
  88. package/dist/typing.d.ts +23 -0
  89. package/dist/typing.js +62 -0
  90. package/dist/version.d.ts +1 -1
  91. package/dist/version.js +1 -1
  92. package/dist/watch.d.ts +78 -1
  93. package/dist/watch.js +109 -0
  94. package/package.json +4 -4
@@ -0,0 +1,21 @@
1
+ import type { SkillSource } from './skill-discovery.js';
2
+ export interface SkillAgentAdapter {
3
+ id: string;
4
+ label: string;
5
+ globalPath: string;
6
+ projectPath: string;
7
+ depth: number;
8
+ /** A folder observation alone never establishes that an agent enables a skill. */
9
+ activation: 'unknown';
10
+ deploymentModes: readonly ['copy'];
11
+ caches: readonly {
12
+ id: string;
13
+ path: string;
14
+ label: string;
15
+ depth: number;
16
+ }[];
17
+ }
18
+ export declare const SKILL_AGENT_ADAPTERS: readonly SkillAgentAdapter[];
19
+ export declare function defaultSkillSources(home?: string): SkillSource[];
20
+ /** Caller explicitly selects/registers the project; never infer access from cwd in desktop. */
21
+ export declare function projectSkillSources(project: string): SkillSource[];
@@ -0,0 +1,19 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve } from 'node:path';
3
+ export const SKILL_AGENT_ADAPTERS = [
4
+ { id: 'agents', label: 'Agents', globalPath: '.agents/skills', projectPath: '.agents/skills', depth: 1, activation: 'unknown', deploymentModes: ['copy'], caches: [] },
5
+ { id: 'codex', label: 'Codex', globalPath: '.codex/skills', projectPath: '.codex/skills', depth: 2, activation: 'unknown', deploymentModes: ['copy'], caches: [{ id: 'codex-plugin-cache', path: '.codex/plugins/cache', label: 'Codex plugin cache', depth: 6 }] },
6
+ { id: 'claude', label: 'Claude', globalPath: '.claude/skills', projectPath: '.claude/skills', depth: 1, activation: 'unknown', deploymentModes: ['copy'], caches: [{ id: 'claude-plugin-cache', path: '.claude/plugins/cache', label: 'Claude plugin cache', depth: 6 }] },
7
+ { id: 'cursor', label: 'Cursor', globalPath: '.cursor/skills', projectPath: '.cursor/skills', depth: 1, activation: 'unknown', deploymentModes: ['copy'], caches: [] },
8
+ ];
9
+ export function defaultSkillSources(home = homedir()) {
10
+ return SKILL_AGENT_ADAPTERS.flatMap(adapter => [
11
+ { id: adapter.id, path: join(home, adapter.globalPath), label: adapter.label, depth: adapter.depth, kind: 'agent' },
12
+ ...adapter.caches.map(cache => ({ id: cache.id, path: join(home, cache.path), label: cache.label, depth: cache.depth, kind: 'cache' })),
13
+ ]);
14
+ }
15
+ /** Caller explicitly selects/registers the project; never infer access from cwd in desktop. */
16
+ export function projectSkillSources(project) {
17
+ const root = resolve(project);
18
+ return SKILL_AGENT_ADAPTERS.map(adapter => ({ id: `project:${root}:${adapter.id}`, path: join(root, adapter.projectPath), label: `${adapter.label} · project`, depth: adapter.depth, kind: 'agent' }));
19
+ }
@@ -0,0 +1,22 @@
1
+ export { defaultSkillSources } from './skill-adapters.js';
2
+ export interface SkillSource {
3
+ path: string;
4
+ label: string;
5
+ depth: number;
6
+ id?: string;
7
+ kind?: "agent" | "cache" | "custom";
8
+ }
9
+ export interface LocalSkill {
10
+ path: string;
11
+ name: string;
12
+ sources: string[];
13
+ aliases: import("./skill-domain.js").SkillAlias[];
14
+ hash?: string;
15
+ fileCount?: number;
16
+ error?: string;
17
+ }
18
+ /** Bounded discovery; canonical aliases share a row. Bundles retain strict link/size checks. */
19
+ export declare function discoverSkills(sources?: SkillSource[]): Promise<{
20
+ skills: LocalSkill[];
21
+ warnings: string[];
22
+ }>;
@@ -0,0 +1,79 @@
1
+ import { readdir, realpath, lstat } from 'node:fs/promises';
2
+ import { defaultSkillSources } from './skill-adapters.js';
3
+ export { defaultSkillSources } from './skill-adapters.js';
4
+ import { join, basename } from 'node:path';
5
+ import { readSkillBundle } from './skills.js';
6
+ class ScanLimitError extends Error {
7
+ }
8
+ /** Bounded discovery; canonical aliases share a row. Bundles retain strict link/size checks. */
9
+ export async function discoverSkills(sources = defaultSkillSources()) {
10
+ const found = new Map();
11
+ const warnings = [];
12
+ let visited = 0;
13
+ for (const source of sources) {
14
+ const seen = new Set();
15
+ async function walk(path, depth) {
16
+ if (++visited > 5000 || found.size >= 1000)
17
+ throw new ScanLimitError('Scan limit reached. Choose a narrower folder to find more skills.');
18
+ const canonical = await realpath(path);
19
+ if (seen.has(canonical)) {
20
+ const existing = found.get(canonical);
21
+ if (existing && !existing.aliases.some(a => a.sourceId === (source.id ?? source.path) && a.path === path))
22
+ existing.aliases.push({ sourceId: source.id ?? source.path, path, label: source.label, kind: source.kind ?? "custom" });
23
+ return;
24
+ }
25
+ seen.add(canonical);
26
+ const entries = await readdir(canonical, { withFileTypes: true });
27
+ if (entries.some(entry => entry.name === 'SKILL.md')) {
28
+ const existing = found.get(canonical);
29
+ if (existing) {
30
+ if (!existing.sources.includes(source.label))
31
+ existing.sources.push(source.label);
32
+ existing.aliases.push({ sourceId: source.id ?? source.path, path, label: source.label, kind: source.kind ?? "custom" });
33
+ return;
34
+ }
35
+ const skill = { path: canonical, name: basename(canonical), sources: [source.label], aliases: [{ sourceId: source.id ?? source.path, path, label: source.label, kind: source.kind ?? "custom" }] };
36
+ found.set(canonical, skill);
37
+ try {
38
+ const bundle = await readSkillBundle(canonical);
39
+ skill.hash = bundle.hash;
40
+ skill.fileCount = bundle.files.length;
41
+ }
42
+ catch (error) {
43
+ skill.error = error instanceof Error ? error.message : String(error);
44
+ }
45
+ return;
46
+ }
47
+ if (depth === 0)
48
+ return;
49
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
50
+ if (['node_modules', '.git'].includes(entry.name))
51
+ continue;
52
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
53
+ continue;
54
+ const child = join(path, entry.name);
55
+ try {
56
+ if ((await lstat(await realpath(child))).isDirectory())
57
+ await walk(child, depth - 1);
58
+ }
59
+ catch (error) {
60
+ if (error instanceof ScanLimitError)
61
+ throw error;
62
+ warnings.push(`${source.label}: ${child}: ${error instanceof Error ? error.message : String(error)}`);
63
+ }
64
+ }
65
+ }
66
+ try {
67
+ await walk(source.path, source.depth);
68
+ }
69
+ catch (error) {
70
+ if (error instanceof ScanLimitError) {
71
+ warnings.push(error.message);
72
+ break;
73
+ }
74
+ if (error.code !== 'ENOENT')
75
+ warnings.push(`${source.label}: ${error instanceof Error ? error.message : String(error)}`);
76
+ }
77
+ }
78
+ return { skills: [...found.values()].sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)), warnings };
79
+ }
@@ -0,0 +1,74 @@
1
+ /** Stable domain records. Names/hashes are labels/content, never identity or permission. */
2
+ export interface SkillIdentity {
3
+ id: string;
4
+ createdAt: number;
5
+ }
6
+ export interface SkillAlias {
7
+ sourceId: string;
8
+ path: string;
9
+ label: string;
10
+ kind: 'agent' | 'cache' | 'custom';
11
+ }
12
+ export interface SkillLocation {
13
+ id: string;
14
+ skillId: string;
15
+ path: string;
16
+ aliases: SkillAlias[];
17
+ createdAt: number;
18
+ }
19
+ export interface SkillObservation {
20
+ locationId: string;
21
+ name: string;
22
+ observedAt: number;
23
+ hash?: string;
24
+ fileCount?: number;
25
+ error?: string;
26
+ availability: 'available' | 'unavailable';
27
+ }
28
+ export interface SkillRevision {
29
+ hash: string;
30
+ contentPolicy: 'bundle-v1';
31
+ cloudVersion?: number;
32
+ }
33
+ export interface CloudSkillIdentity {
34
+ deployment: string;
35
+ organizationId: string;
36
+ skillId: string;
37
+ }
38
+ export interface SkillCloudBinding {
39
+ id: string;
40
+ localSkillId: string;
41
+ remote: CloudSkillIdentity & {
42
+ projectId: string;
43
+ };
44
+ /** Authenticated member ID, never an API key or its fingerprint. */
45
+ accountId: string;
46
+ /** Lookup hints only; every lookup must revalidate the qualified IDs. */
47
+ locator: {
48
+ project: string;
49
+ name: string;
50
+ };
51
+ baseline?: SkillRevision;
52
+ }
53
+ export interface SkillProjectAssignment {
54
+ projectId: string;
55
+ remote: CloudSkillIdentity;
56
+ pinnedVersion: number;
57
+ overrideSkillId?: string;
58
+ }
59
+ export interface SkillAgentTarget {
60
+ skillId: string;
61
+ agentId: string;
62
+ path: string;
63
+ scope: 'global' | 'project';
64
+ mode: 'copy' | 'link';
65
+ ownership: 'managed' | 'unmanaged' | 'unknown';
66
+ }
67
+ export interface SkillOperation {
68
+ id: string;
69
+ kind: 'push' | 'pull' | 'install';
70
+ skillId: string;
71
+ expectedLocalHash?: string;
72
+ expectedRemote?: SkillRevision;
73
+ status: 'planned' | 'running' | 'succeeded' | 'failed' | 'cancelled';
74
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { SkillOperationJournal, type SkillPushPorts } from './skill-operations.js';
2
+ import type { SkillStore } from './skill-store.js';
3
+ export declare class SkillOperationExecutor {
4
+ readonly store: SkillStore;
5
+ readonly journal: SkillOperationJournal;
6
+ readonly cloud: SkillPushPorts;
7
+ constructor(store: SkillStore, journal: SkillOperationJournal, cloud: SkillPushPorts);
8
+ apply(id: string, recover?: boolean): Promise<import("./skill-operations.js").SkillTransferOperation>;
9
+ /** Each item owns its durable outcome. Successful work is never replayed. */
10
+ batch(ids: string[], options?: {
11
+ recover?: boolean;
12
+ signal?: AbortSignal;
13
+ onResult?: (result: {
14
+ id: string;
15
+ status: string;
16
+ error?: string;
17
+ }) => void;
18
+ }): Promise<{
19
+ id: string;
20
+ status: string;
21
+ error?: string;
22
+ }[]>;
23
+ }
@@ -0,0 +1,51 @@
1
+ import { SkillPushExecutor, operationDirection } from './skill-operations.js';
2
+ import { SkillLocalExecutor } from './skill-local-executor.js';
3
+ export class SkillOperationExecutor {
4
+ store;
5
+ journal;
6
+ cloud;
7
+ constructor(store, journal, cloud) {
8
+ this.store = store;
9
+ this.journal = journal;
10
+ this.cloud = cloud;
11
+ }
12
+ async apply(id, recover = false) {
13
+ const op = (await this.journal.list()).find(o => o.id === id);
14
+ if (!op)
15
+ throw new Error('Operation not found.');
16
+ return operationDirection(op.plan) === 'push'
17
+ ? new SkillPushExecutor(this.store, this.journal, this.cloud).apply(id, recover)
18
+ : new SkillLocalExecutor(this.store, this.journal, this.cloud).apply(id, recover);
19
+ }
20
+ /** Each item owns its durable outcome. Successful work is never replayed. */
21
+ async batch(ids, options = {}) {
22
+ if (!Array.isArray(ids) || !ids.length || ids.length > 1000 || new Set(ids).size !== ids.length || ids.some(id => typeof id !== 'string'))
23
+ throw new Error('Select 1–1000 distinct operation IDs.');
24
+ const operations = await this.journal.list();
25
+ if (ids.some(id => !operations.some(op => op.id === id)))
26
+ throw new Error('A selected operation no longer exists. Refresh before running.');
27
+ const results = [];
28
+ for (const id of ids) {
29
+ let result;
30
+ if (options.signal?.aborted) {
31
+ const op = (await this.journal.list()).find(o => o.id === id);
32
+ if (op.status === 'planned')
33
+ result = { id, status: (await this.journal.cancel(id)).status };
34
+ else
35
+ result = { id, status: op.status, error: ['succeeded', 'cancelled'].includes(op.status) ? undefined : 'Batch stopped before this item; its previous outcome is retained.' };
36
+ }
37
+ else {
38
+ try {
39
+ result = { id, status: (await this.apply(id, options.recover)).status };
40
+ }
41
+ catch (error) {
42
+ const current = (await this.journal.list()).find(o => o.id === id);
43
+ result = { id, status: current.status, error: error.message };
44
+ }
45
+ }
46
+ results.push(result);
47
+ options.onResult?.(result);
48
+ }
49
+ return results;
50
+ }
51
+ }
@@ -0,0 +1,54 @@
1
+ import { SkillService } from './skill-service.js';
2
+ /** Events are hints; periodic reconciliation repairs missed events and root changes. */
3
+ export declare class SkillIndex {
4
+ readonly service: SkillService;
5
+ readonly changed: () => void;
6
+ readonly reconciliationMs: number;
7
+ private watchers;
8
+ private timer?;
9
+ private interval?;
10
+ private pending;
11
+ private full;
12
+ private running;
13
+ private active?;
14
+ private closed;
15
+ private started?;
16
+ private warnings;
17
+ constructor(service: SkillService, changed?: () => void, reconciliationMs?: number);
18
+ start(): Promise<void>;
19
+ inventory(): Promise<{
20
+ warnings: string[];
21
+ skills: {
22
+ skillId: string;
23
+ locationId: string;
24
+ path: string;
25
+ name: string;
26
+ sources: string[];
27
+ aliases: import("./skill-domain.js").SkillAlias[];
28
+ hash: string | undefined;
29
+ fileCount: number | undefined;
30
+ error: string | undefined;
31
+ availability: "available" | "unavailable";
32
+ }[];
33
+ }>;
34
+ invalidate(path?: string): void;
35
+ private configure;
36
+ refresh(): Promise<{
37
+ warnings: string[];
38
+ skills: {
39
+ skillId: string;
40
+ locationId: string;
41
+ path: string;
42
+ name: string;
43
+ sources: string[];
44
+ aliases: import("./skill-domain.js").SkillAlias[];
45
+ hash: string | undefined;
46
+ fileCount: number | undefined;
47
+ error: string | undefined;
48
+ availability: "available" | "unavailable";
49
+ }[];
50
+ }>;
51
+ private flush;
52
+ private perform;
53
+ close(): void;
54
+ }
@@ -0,0 +1,115 @@
1
+ import { watch } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { pathWithin } from './skill-service.js';
4
+ /** Events are hints; periodic reconciliation repairs missed events and root changes. */
5
+ export class SkillIndex {
6
+ service;
7
+ changed;
8
+ reconciliationMs;
9
+ watchers = [];
10
+ timer;
11
+ interval;
12
+ pending = new Set();
13
+ full = false;
14
+ running = false;
15
+ active;
16
+ closed = false;
17
+ started;
18
+ warnings = [];
19
+ constructor(service, changed = () => { }, reconciliationMs = 60_000) {
20
+ this.service = service;
21
+ this.changed = changed;
22
+ this.reconciliationMs = reconciliationMs;
23
+ }
24
+ start() {
25
+ return this.started ??= (async () => {
26
+ this.full = true;
27
+ await this.flush();
28
+ if (!this.closed) {
29
+ this.interval = setInterval(() => this.invalidate(), this.reconciliationMs);
30
+ this.interval.unref();
31
+ }
32
+ })();
33
+ }
34
+ async inventory() { await this.start(); return { ...await this.service.cachedInventory(), warnings: this.warnings }; }
35
+ invalidate(path) {
36
+ if (this.closed)
37
+ return;
38
+ const storePath = 'path' in this.service.store ? String(this.service.store.path) : undefined;
39
+ if (path && storePath && (path === storePath || path.startsWith(`${storePath}-`)))
40
+ return;
41
+ if (path && this.pending.size < 1000)
42
+ this.pending.add(path);
43
+ else
44
+ this.full = true;
45
+ if (!this.timer)
46
+ this.timer = setTimeout(() => { this.timer = undefined; void this.flush(); }, 150);
47
+ }
48
+ async configure() {
49
+ for (const watcher of this.watchers)
50
+ watcher.close();
51
+ this.watchers = [];
52
+ const sources = await this.service.discoverySources();
53
+ for (const source of sources.slice(0, 128)) {
54
+ try {
55
+ const watcher = watch(source.path, { recursive: true, persistent: false }, (_event, name) => this.invalidate(name ? resolve(source.path, String(name)) : undefined));
56
+ watcher.on('error', () => this.invalidate());
57
+ this.watchers.push(watcher);
58
+ }
59
+ catch { /* Missing roots are retried by reconciliation, never deleted from the catalog. */ }
60
+ }
61
+ }
62
+ async refresh() {
63
+ await this.start();
64
+ if (this.active)
65
+ await this.active;
66
+ this.full = true;
67
+ await this.flush();
68
+ return { ...await this.service.cachedInventory(), warnings: this.warnings };
69
+ }
70
+ flush() {
71
+ if (this.active)
72
+ return this.active;
73
+ this.active = this.perform().finally(() => { this.active = undefined; });
74
+ return this.active;
75
+ }
76
+ async perform() {
77
+ if (this.running || this.closed)
78
+ return;
79
+ this.running = true;
80
+ const full = this.full;
81
+ this.full = false;
82
+ const paths = [...this.pending];
83
+ this.pending.clear();
84
+ let failed = false;
85
+ try {
86
+ const catalog = await this.service.store.read();
87
+ const ids = catalog.locations.filter(l => paths.some(p => pathWithin(l.path, p))).map(l => l.id);
88
+ const unknown = paths.some(p => !catalog.locations.some(l => pathWithin(l.path, p)));
89
+ if (full || unknown) {
90
+ const result = await this.service.inventory();
91
+ this.warnings = result.warnings;
92
+ await this.configure();
93
+ }
94
+ else if (ids.length)
95
+ await this.service.refreshLocations(ids);
96
+ if (!this.closed)
97
+ this.changed();
98
+ }
99
+ catch (error) {
100
+ failed = true;
101
+ this.warnings = [error.message];
102
+ this.full = true;
103
+ if (!this.closed)
104
+ this.changed();
105
+ }
106
+ finally {
107
+ this.running = false;
108
+ }
109
+ // Do not drop events received during inspection, including external edits during our own writes.
110
+ if ((this.pending.size || (this.full && !failed)) && !this.closed && !this.timer)
111
+ this.timer = setTimeout(() => { this.timer = undefined; void this.flush(); }, 150);
112
+ }
113
+ close() { this.closed = true; clearTimeout(this.timer); clearInterval(this.interval); for (const watcher of this.watchers)
114
+ watcher.close(); this.watchers = []; }
115
+ }
@@ -0,0 +1,18 @@
1
+ import { type PublishedSkillSnapshot } from './skill-sync.js';
2
+ import { SkillOperationJournal, type SkillTransferOperation } from './skill-operations.js';
3
+ import type { SkillStore } from './skill-store.js';
4
+ export interface SkillLocalPorts {
5
+ snapshot(project: string, name: string): Promise<PublishedSkillSnapshot>;
6
+ /** Fault/telemetry seam, after durable steps; production never infers success from it. */
7
+ checkpoint?: (phase: string) => Promise<void>;
8
+ }
9
+ /** Recoverable local installation. Old bytes remain in an operation-specific backup.
10
+ * Destination creation and every file publication are exclusive: no foreign file is overwritten. */
11
+ export declare class SkillLocalExecutor {
12
+ readonly store: SkillStore;
13
+ readonly journal: SkillOperationJournal;
14
+ readonly cloud: SkillLocalPorts;
15
+ constructor(store: SkillStore, journal: SkillOperationJournal, cloud: SkillLocalPorts);
16
+ apply(id: string, recover?: boolean): Promise<SkillTransferOperation>;
17
+ private run;
18
+ }