runwork 0.11.0 → 0.13.1
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/agents/__tests__/intro-skill.test.js +32 -0
- package/dist/agents/claude-code.d.ts +21 -0
- package/dist/agents/claude-code.js +58 -2
- package/dist/agents/claude-desktop-plugin-tree.d.ts +15 -0
- package/dist/agents/claude-desktop-plugin-tree.js +37 -1
- package/dist/agents/conversation-skills.d.ts +18 -0
- package/dist/agents/conversation-skills.js +229 -0
- package/dist/agents/intro-skill.d.ts +9 -0
- package/dist/agents/intro-skill.js +41 -0
- package/dist/agents/registry-data.d.ts +35 -0
- package/dist/agents/registry-data.js +58 -1
- package/dist/agents/runtime-detection.d.ts +82 -0
- package/dist/agents/runtime-detection.js +271 -0
- package/dist/agents/session-start-hook.d.ts +37 -0
- package/dist/agents/session-start-hook.js +159 -0
- package/dist/agents/types.d.ts +12 -0
- package/dist/api/client.d.ts +47 -0
- package/dist/api/client.js +32 -0
- package/dist/commands/__tests__/setup-persona.test.d.ts +1 -0
- package/dist/commands/__tests__/setup-persona.test.js +31 -0
- package/dist/commands/doctor.js +89 -2
- package/dist/commands/inbox.d.ts +11 -0
- package/dist/commands/inbox.js +60 -0
- package/dist/commands/info.d.ts +1 -1
- package/dist/commands/info.js +7 -2
- package/dist/commands/resume.d.ts +2 -0
- package/dist/commands/resume.js +265 -0
- package/dist/commands/save-convo.d.ts +8 -0
- package/dist/commands/save-convo.js +20 -0
- package/dist/commands/setup.d.ts +7 -0
- package/dist/commands/setup.js +22 -0
- package/dist/commands/share-convo.d.ts +24 -0
- package/dist/commands/share-convo.js +167 -0
- package/dist/commands/sync.js +96 -10
- package/dist/generated/bundled-types.js +33 -33
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.js +8 -0
- package/dist/types.d.ts +13 -0
- package/dist/utils/app-info.d.ts +4 -0
- package/dist/utils/app-info.js +17 -0
- package/package.json +1 -1
package/dist/commands/setup.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { ApiClient } from '../api/client.js';
|
|
3
|
+
import type { SetupState } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the persona to persist in setup.json. Prefers the `--persona` flag
|
|
6
|
+
* (passed by the desktop onboarding flow); otherwise preserves any persona
|
|
7
|
+
* already on disk so re-running `setup` without the flag does not wipe it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolvePersona(flag: string | undefined, existing: SetupState['persona']): SetupState['persona'];
|
|
3
10
|
/**
|
|
4
11
|
* Resolve the workspace for `setup` and persist it as the default in credentials.
|
|
5
12
|
*
|
package/dist/commands/setup.js
CHANGED
|
@@ -8,6 +8,26 @@ import { resolveWorkspace, hasProjectConfig } from '../workspace/resolve.js';
|
|
|
8
8
|
import { promptSelect, promptConfirm } from '../utils/prompt.js';
|
|
9
9
|
import { detectAgents, printNoAgentsMessage } from '../agents/detect.js';
|
|
10
10
|
import { syncFromState } from './sync.js';
|
|
11
|
+
import { loadSetupState } from '../utils/setup-state.js';
|
|
12
|
+
const PERSONA_LABELS = {
|
|
13
|
+
1: 'novice',
|
|
14
|
+
2: 'curious',
|
|
15
|
+
3: 'engineer',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the persona to persist in setup.json. Prefers the `--persona` flag
|
|
19
|
+
* (passed by the desktop onboarding flow); otherwise preserves any persona
|
|
20
|
+
* already on disk so re-running `setup` without the flag does not wipe it.
|
|
21
|
+
*/
|
|
22
|
+
export function resolvePersona(flag, existing) {
|
|
23
|
+
if (flag) {
|
|
24
|
+
const level = Number(flag);
|
|
25
|
+
if (level === 1 || level === 2 || level === 3) {
|
|
26
|
+
return { level, label: PERSONA_LABELS[level] };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return existing;
|
|
30
|
+
}
|
|
11
31
|
/**
|
|
12
32
|
* Resolve the workspace for `setup` and persist it as the default in credentials.
|
|
13
33
|
*
|
|
@@ -49,6 +69,7 @@ export const setupCommand = new Command('setup')
|
|
|
49
69
|
.option('--agent <slug>', 'Only configure a specific agent (e.g. claude-code, cursor)')
|
|
50
70
|
.option('--dry-run', 'Show what would be configured without writing files')
|
|
51
71
|
.option('-y, --yes', 'Skip all prompts, configure all detected agents with user scope')
|
|
72
|
+
.option('--persona <level>', 'Technical-level persona for agent instructions (1=novice, 2=curious, 3=engineer)')
|
|
52
73
|
.action(async (opts) => {
|
|
53
74
|
const credentials = requireAuth();
|
|
54
75
|
const client = new ApiClient(credentials);
|
|
@@ -124,6 +145,7 @@ export const setupCommand = new Command('setup')
|
|
|
124
145
|
skills: [],
|
|
125
146
|
skillHashes: {},
|
|
126
147
|
lastDetectedAt: new Date().toISOString(),
|
|
148
|
+
persona: resolvePersona(opts.persona, loadSetupState()?.persona),
|
|
127
149
|
};
|
|
128
150
|
const scopes = scope === 'both' ? ['project', 'user'] : [scope];
|
|
129
151
|
for (const s of scopes) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* `runwork share-convo` - share the current AI conversation with a teammate.
|
|
4
|
+
*
|
|
5
|
+
* Typically invoked by the `share-conversation` skill from inside an AI agent.
|
|
6
|
+
* The skill is responsible for emitting the markdown transcript and metadata;
|
|
7
|
+
* this command does the upload, host-agent detection, and native-bundle pickup.
|
|
8
|
+
*/
|
|
9
|
+
interface ShareConvoOptions {
|
|
10
|
+
to?: string[];
|
|
11
|
+
transcriptFile?: string;
|
|
12
|
+
nativeFile?: string;
|
|
13
|
+
sourceAgent?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
note?: string;
|
|
16
|
+
personal?: boolean;
|
|
17
|
+
ttlDays?: string;
|
|
18
|
+
metadataJson?: string;
|
|
19
|
+
metadataFile?: string;
|
|
20
|
+
workspace?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function runShareConvo(opts: ShareConvoOptions, command: Command, isPersonalAlias?: boolean): Promise<void>;
|
|
23
|
+
export declare const shareConvoCommand: Command;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { requireAuth } from '../auth/store.js';
|
|
5
|
+
import { ApiClient } from '../api/client.js';
|
|
6
|
+
import { resolveWorkspace } from '../workspace/resolve.js';
|
|
7
|
+
import { shouldOutputJson, jsonOut } from '../utils/output.js';
|
|
8
|
+
import { detectCurrentAgent } from '../agents/runtime-detection.js';
|
|
9
|
+
function nativeBundleFormatForAgent(slug) {
|
|
10
|
+
if (slug === 'claude-code' || slug === 'claude-desktop')
|
|
11
|
+
return 'claude-jsonl';
|
|
12
|
+
if (slug === 'codex' || slug === 'codex-app')
|
|
13
|
+
return 'codex-rollout';
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
function sha256Hex(content) {
|
|
17
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
18
|
+
}
|
|
19
|
+
function utf8ByteLength(content) {
|
|
20
|
+
return Buffer.byteLength(content, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
export async function runShareConvo(opts, command, isPersonalAlias = false) {
|
|
23
|
+
const useJson = shouldOutputJson(command.optsWithGlobals().json);
|
|
24
|
+
if (!opts.transcriptFile) {
|
|
25
|
+
console.error('Error: --transcript-file is required. Pass the path to the LLM-emitted markdown transcript.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
if (!existsSync(opts.transcriptFile)) {
|
|
29
|
+
console.error(`Error: transcript file does not exist: ${opts.transcriptFile}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (!opts.title) {
|
|
33
|
+
console.error('Error: --title is required.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const isPersonal = isPersonalAlias || !!opts.personal || (!opts.to || opts.to.length === 0);
|
|
37
|
+
const recipients = opts.to ?? [];
|
|
38
|
+
if (!isPersonal && recipients.length === 0) {
|
|
39
|
+
console.error('Error: at least one --to <email> is required (or use --personal to save for yourself)');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
if (isPersonal && recipients.length > 0) {
|
|
43
|
+
console.error('Error: --personal and --to cannot be combined; personal shares are self-only');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const credentials = requireAuth();
|
|
47
|
+
const client = new ApiClient(credentials);
|
|
48
|
+
const { workspaceId } = await resolveWorkspace(client, { workspace: opts.workspace });
|
|
49
|
+
// Read transcript bundle (always required)
|
|
50
|
+
const transcriptContent = readFileSync(opts.transcriptFile, 'utf8');
|
|
51
|
+
const bundles = [
|
|
52
|
+
{
|
|
53
|
+
format: 'transcript',
|
|
54
|
+
content: transcriptContent,
|
|
55
|
+
sizeBytes: utf8ByteLength(transcriptContent),
|
|
56
|
+
sha256: sha256Hex(transcriptContent),
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
// Detect host agent (for source tagging and native bundle pickup)
|
|
60
|
+
const detected = detectCurrentAgent();
|
|
61
|
+
const sourceAgent = opts.sourceAgent ?? detected?.slug ?? 'generic';
|
|
62
|
+
// Resolve native bundle - explicit --native-file wins, else use detection
|
|
63
|
+
let nativeFilePath = null;
|
|
64
|
+
if (opts.nativeFile) {
|
|
65
|
+
if (!existsSync(opts.nativeFile)) {
|
|
66
|
+
console.error(`Error: --native-file path does not exist: ${opts.nativeFile}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
nativeFilePath = opts.nativeFile;
|
|
70
|
+
}
|
|
71
|
+
else if (detected?.sessionFilePath && existsSync(detected.sessionFilePath)) {
|
|
72
|
+
nativeFilePath = detected.sessionFilePath;
|
|
73
|
+
}
|
|
74
|
+
if (nativeFilePath) {
|
|
75
|
+
const nativeFormat = nativeBundleFormatForAgent(sourceAgent);
|
|
76
|
+
if (nativeFormat) {
|
|
77
|
+
try {
|
|
78
|
+
const content = readFileSync(nativeFilePath, 'utf8');
|
|
79
|
+
bundles.push({
|
|
80
|
+
format: nativeFormat,
|
|
81
|
+
content,
|
|
82
|
+
sizeBytes: utf8ByteLength(content),
|
|
83
|
+
sha256: sha256Hex(content),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error(`Warning: could not read native file ${nativeFilePath}: ${err instanceof Error ? err.message : err}`);
|
|
88
|
+
console.error('Continuing with transcript-only bundle.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Metadata - merge --metadata-json and --metadata-file
|
|
93
|
+
let metadata = {};
|
|
94
|
+
if (opts.metadataFile) {
|
|
95
|
+
try {
|
|
96
|
+
metadata = JSON.parse(readFileSync(opts.metadataFile, 'utf8'));
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error(`Error: --metadata-file is not valid JSON: ${err instanceof Error ? err.message : err}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (opts.metadataJson) {
|
|
104
|
+
try {
|
|
105
|
+
metadata = { ...metadata, ...JSON.parse(opts.metadataJson) };
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.error(`Error: --metadata-json is not valid JSON: ${err instanceof Error ? err.message : err}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Augment metadata with detected host info if not already provided
|
|
113
|
+
if (detected && !metadata.sourceSessionId) {
|
|
114
|
+
metadata.sourceSessionId = detected.sessionId;
|
|
115
|
+
}
|
|
116
|
+
if (!metadata.os) {
|
|
117
|
+
metadata.os = process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'windows' : 'linux';
|
|
118
|
+
}
|
|
119
|
+
const ttlDays = opts.ttlDays ? parseInt(opts.ttlDays, 10) : undefined;
|
|
120
|
+
try {
|
|
121
|
+
const result = await client.createSharedConversation(workspaceId, {
|
|
122
|
+
recipients: isPersonal ? [] : recipients,
|
|
123
|
+
isPersonal,
|
|
124
|
+
sourceAgent,
|
|
125
|
+
title: opts.title,
|
|
126
|
+
note: opts.note,
|
|
127
|
+
bundles,
|
|
128
|
+
metadata,
|
|
129
|
+
ttlDays: ttlDays && Number.isFinite(ttlDays) ? ttlDays : undefined,
|
|
130
|
+
});
|
|
131
|
+
if (useJson) {
|
|
132
|
+
jsonOut(result);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (isPersonal) {
|
|
136
|
+
console.log(`Saved conversation: ${result.share.title}`);
|
|
137
|
+
console.log(`Share ID: ${result.share.id}`);
|
|
138
|
+
console.log(`Expires: ${result.share.expiresAt}`);
|
|
139
|
+
console.log(`\nResume with: runwork resume ${result.share.id}`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log(`Shared with ${result.sharedCount} member${result.sharedCount === 1 ? '' : 's'}` +
|
|
143
|
+
(result.invitedCount > 0 ? ` and invited ${result.invitedCount} new user${result.invitedCount === 1 ? '' : 's'}` : '') + '.');
|
|
144
|
+
console.log(`Share ID: ${result.share.id}`);
|
|
145
|
+
if (result.skipped.length > 0) {
|
|
146
|
+
console.log(`\nSkipped: ${result.skipped.map(s => `${s.identifier} (${s.reason})`).join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.error(`Failed to share conversation: ${err instanceof Error ? err.message : err}`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export const shareConvoCommand = new Command('share-convo')
|
|
156
|
+
.description('Share the current AI conversation with a teammate')
|
|
157
|
+
.option('--to <email>', 'Recipient email (repeatable)', (value, prev = []) => [...prev, value], [])
|
|
158
|
+
.option('--transcript-file <path>', 'Path to the LLM-emitted markdown transcript (required)')
|
|
159
|
+
.option('--native-file <path>', 'Path to the native session file (optional; auto-detected from env vars otherwise)')
|
|
160
|
+
.option('--source-agent <slug>', 'Override host-agent detection (e.g. claude-code, codex, claude-desktop)')
|
|
161
|
+
.option('--title <string>', 'Short title for the conversation (required)')
|
|
162
|
+
.option('--note <string>', 'Optional personal note to recipients')
|
|
163
|
+
.option('--ttl-days <n>', 'Days until expiration (1-30, default 7)')
|
|
164
|
+
.option('--metadata-json <json>', 'Inline JSON object with workMode, openQuestions, suggestedNextStep, etc.')
|
|
165
|
+
.option('--metadata-file <path>', 'Path to a JSON file with the same metadata fields')
|
|
166
|
+
.option('--workspace <id>', 'Workspace ID')
|
|
167
|
+
.action((opts, command) => runShareConvo(opts, command, false));
|
package/dist/commands/sync.js
CHANGED
|
@@ -11,6 +11,7 @@ import { RUNWORK_AGENT_DEFAULTS, AGENT_DEFAULTS_SCHEMA_VERSION } from '../agents
|
|
|
11
11
|
import { resolveAgentDefaults } from '../agents/defaults-merge.js';
|
|
12
12
|
import { collectTelemetryEvents, printTelemetryVerbose, summarizeTelemetryForDryRun, } from './sync-telemetry.js';
|
|
13
13
|
import { generateIntroSkill, generateInstructionHint, buildAppSkillDescription } from '../agents/intro-skill.js';
|
|
14
|
+
import { buildShareConversationSkill, buildSaveConversationSkill, BUILT_IN_SKILL_NAMES } from '../agents/conversation-skills.js';
|
|
14
15
|
import { computeSyncPlan } from '../sync/change-detect.js';
|
|
15
16
|
import { printSyncSummary, resolveConflict, resolveConflictNonInteractive } from '../sync/conflict-ui.js';
|
|
16
17
|
import { executeSyncPlan } from '../sync/executor.js';
|
|
@@ -194,11 +195,27 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
194
195
|
}
|
|
195
196
|
// Read local skills for change detection
|
|
196
197
|
const localSkills = readLocalSkills(state);
|
|
198
|
+
// Built-in skills (shipped by the CLI from code) are CANONICAL: they
|
|
199
|
+
// are never pushed to the workspace and never pulled from it. The diff
|
|
200
|
+
// engine must not see them at all -- if it did, a workspace skill that
|
|
201
|
+
// happens to share a name (e.g. someone uploaded `share-conversation`
|
|
202
|
+
// manually) would either push the built-in upstream or pull the stale
|
|
203
|
+
// workspace copy on top, both wrong. Filter both sides before the diff.
|
|
204
|
+
const builtInNameSet = new Set(BUILT_IN_SKILL_NAMES);
|
|
205
|
+
const collidingRemote = remoteSkills.filter(s => builtInNameSet.has(s.name));
|
|
206
|
+
if (collidingRemote.length > 0) {
|
|
207
|
+
console.log(`\n Warning: ${collidingRemote.length} workspace skill${collidingRemote.length === 1 ? '' : 's'} ` +
|
|
208
|
+
`shadow${collidingRemote.length === 1 ? 's' : ''} built-in CLI skill${collidingRemote.length === 1 ? '' : 's'}: ` +
|
|
209
|
+
`${collidingRemote.map(s => s.name).join(', ')}. The CLI built-in versions will be used locally; ` +
|
|
210
|
+
`the workspace copies are stale duplicates and can be safely deleted from the dashboard.`);
|
|
211
|
+
}
|
|
212
|
+
const localSkillsForDiff = localSkills.filter(s => !builtInNameSet.has(s.name));
|
|
213
|
+
const remoteSkillsForDiff = remoteSkills.filter(s => !builtInNameSet.has(s.name));
|
|
197
214
|
// Compute sync plan
|
|
198
215
|
const plan = computeSyncPlan({
|
|
199
216
|
storedHashes: state.skillHashes || {},
|
|
200
|
-
localSkills,
|
|
201
|
-
remoteSkills,
|
|
217
|
+
localSkills: localSkillsForDiff,
|
|
218
|
+
remoteSkills: remoteSkillsForDiff,
|
|
202
219
|
});
|
|
203
220
|
// In pull-only mode, move pushes and conflicts to skips
|
|
204
221
|
if (opts.pullOnly) {
|
|
@@ -293,6 +310,7 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
293
310
|
appCount: allSkills.filter(s => s.type === 'app').length,
|
|
294
311
|
skillCount: remoteSkills.length,
|
|
295
312
|
mcpServerCount: mcpEntries.length,
|
|
313
|
+
persona: state.persona,
|
|
296
314
|
});
|
|
297
315
|
// For project scope: determine which app skill to include (only current app's skill)
|
|
298
316
|
let projectAppSkillFilter = null;
|
|
@@ -305,7 +323,28 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
305
323
|
}
|
|
306
324
|
catch { /* ignore */ }
|
|
307
325
|
}
|
|
326
|
+
// Built-in skills shipped by the CLI on every sync. Named upfront so we can
|
|
327
|
+
// surface them by name in the per-adapter sync log -- helps users (and
|
|
328
|
+
// their agents) confirm that /runwork, /share-conversation, etc. landed.
|
|
329
|
+
const builtInSkills = [
|
|
330
|
+
introSkill,
|
|
331
|
+
buildShareConversationSkill(),
|
|
332
|
+
buildSaveConversationSkill(),
|
|
333
|
+
];
|
|
334
|
+
const builtInNames = builtInSkills.map(s => s.name);
|
|
335
|
+
// Counters surfaced at the end of the sync as a one-line summary so the
|
|
336
|
+
// user can see at a glance how much actually got written this run.
|
|
337
|
+
const summary = {
|
|
338
|
+
adaptersProcessed: 0,
|
|
339
|
+
adaptersFailed: 0,
|
|
340
|
+
skillWrites: 0,
|
|
341
|
+
skillFilesWritten: 0,
|
|
342
|
+
mcpServerWrites: 0,
|
|
343
|
+
instructionHintWrites: 0,
|
|
344
|
+
hookInstallCalls: 0,
|
|
345
|
+
};
|
|
308
346
|
for (const adapter of adapters) {
|
|
347
|
+
let adapterFailedAnyScope = false;
|
|
309
348
|
for (const scope of scopes) {
|
|
310
349
|
try {
|
|
311
350
|
// Write skills: for project scope, filter to only the current app's skill.
|
|
@@ -315,6 +354,11 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
315
354
|
if (adapter.supportsSkills()) {
|
|
316
355
|
const skipAppSkills = adapter.mcpProvidesSkills && mcpEntries.length > 0;
|
|
317
356
|
const scopeSkills = remoteSkills.filter(s => {
|
|
357
|
+
// Built-in CLI skills are written from the canonical built-in array
|
|
358
|
+
// below; a workspace skill with the same name is a shadow that
|
|
359
|
+
// would overwrite the built-in if we let it through. Skip it.
|
|
360
|
+
if (builtInNameSet.has(s.name))
|
|
361
|
+
return false;
|
|
318
362
|
// App-sourced skills are already available via MCP skill_* tools
|
|
319
363
|
if (skipAppSkills && s.source === 'app')
|
|
320
364
|
return false;
|
|
@@ -324,29 +368,57 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
324
368
|
}
|
|
325
369
|
return true;
|
|
326
370
|
});
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
371
|
+
const workspaceSkillFiles = scopeSkills.map(s => ({
|
|
372
|
+
name: s.name,
|
|
373
|
+
filename: s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
374
|
+
content: s.content,
|
|
375
|
+
description: s.source === 'app'
|
|
376
|
+
? (buildAppSkillDescription(s.name, registries) || `${s.name} - Runwork workspace application`)
|
|
377
|
+
: `${s.name} - Runwork workspace skill`,
|
|
378
|
+
}));
|
|
379
|
+
const allSkillFiles = [...builtInSkills, ...workspaceSkillFiles];
|
|
335
380
|
await adapter.writeSkills(allSkillFiles, scope);
|
|
381
|
+
summary.skillWrites++;
|
|
382
|
+
summary.skillFilesWritten += allSkillFiles.length;
|
|
383
|
+
console.log(` [${adapter.name}] Wrote ${allSkillFiles.length} skills (${scope}): ` +
|
|
384
|
+
`${builtInSkills.length} built-in [${builtInNames.join(', ')}], ` +
|
|
385
|
+
`${workspaceSkillFiles.length} workspace`);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
console.log(` [${adapter.name}] Skipped skills (${scope}): adapter does not support skills`);
|
|
336
389
|
}
|
|
337
390
|
// Write MCP configs
|
|
338
391
|
if (adapter.supportsMcpScope(scope) && mcpEntries.length > 0) {
|
|
339
392
|
await adapter.writeMcpServers(mcpEntries, scope);
|
|
393
|
+
summary.mcpServerWrites++;
|
|
340
394
|
console.log(` [${adapter.name}] Updated ${mcpEntries.length} MCP server${mcpEntries.length > 1 ? 's' : ''} (${scope})`);
|
|
341
395
|
}
|
|
396
|
+
else if (mcpEntries.length === 0) {
|
|
397
|
+
console.log(` [${adapter.name}] Skipped MCP servers (${scope}): no workspace MCP servers configured`);
|
|
398
|
+
}
|
|
399
|
+
else if (!adapter.supportsMcpScope(scope)) {
|
|
400
|
+
console.log(` [${adapter.name}] Skipped MCP servers (${scope}): adapter does not support MCP at this scope`);
|
|
401
|
+
}
|
|
342
402
|
// Write instruction hint
|
|
343
403
|
await adapter.writeInstructionHint(instructionHint, scope);
|
|
404
|
+
summary.instructionHintWrites++;
|
|
344
405
|
console.log(` [${adapter.name}] Updated instruction hints (${scope})`);
|
|
406
|
+
// Install built-in hooks (Claude Code's SessionStart, etc.). Called
|
|
407
|
+
// after writeSkills so the plugin tree already exists. Skipped by
|
|
408
|
+
// adapters that don't implement it.
|
|
409
|
+
if (adapter.writeBuiltInHooks) {
|
|
410
|
+
await adapter.writeBuiltInHooks(scope);
|
|
411
|
+
summary.hookInstallCalls++;
|
|
412
|
+
}
|
|
345
413
|
}
|
|
346
414
|
catch (err) {
|
|
415
|
+
adapterFailedAnyScope = true;
|
|
347
416
|
console.warn(` [${adapter.name}] Failed (${scope}): ${err instanceof Error ? err.message : err}`);
|
|
348
417
|
}
|
|
349
418
|
}
|
|
419
|
+
summary.adaptersProcessed++;
|
|
420
|
+
if (adapterFailedAnyScope)
|
|
421
|
+
summary.adaptersFailed++;
|
|
350
422
|
}
|
|
351
423
|
// Pull team config from server. A failure here must not stop the
|
|
352
424
|
// unified user-scope write below, which applies network and minimum-permission
|
|
@@ -568,6 +640,20 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
568
640
|
catch {
|
|
569
641
|
// Telemetry failures are non-fatal
|
|
570
642
|
}
|
|
643
|
+
// One-line summary so the user (and any agent reading sync output) can
|
|
644
|
+
// see at a glance what got written this run, without scrolling the whole
|
|
645
|
+
// per-adapter log.
|
|
646
|
+
const summaryParts = [];
|
|
647
|
+
summaryParts.push(`${summary.adaptersProcessed} adapter${summary.adaptersProcessed === 1 ? '' : 's'}`);
|
|
648
|
+
if (summary.adaptersFailed > 0)
|
|
649
|
+
summaryParts.push(`${summary.adaptersFailed} failed`);
|
|
650
|
+
summaryParts.push(`${summary.skillWrites} skill write${summary.skillWrites === 1 ? '' : 's'} (${summary.skillFilesWritten} files)`);
|
|
651
|
+
if (summary.mcpServerWrites > 0)
|
|
652
|
+
summaryParts.push(`${summary.mcpServerWrites} MCP config write${summary.mcpServerWrites === 1 ? '' : 's'}`);
|
|
653
|
+
if (summary.hookInstallCalls > 0)
|
|
654
|
+
summaryParts.push(`${summary.hookInstallCalls} hook install${summary.hookInstallCalls === 1 ? '' : 's'}`);
|
|
655
|
+
summaryParts.push(`${summary.instructionHintWrites} instruction hint write${summary.instructionHintWrites === 1 ? '' : 's'}`);
|
|
656
|
+
console.log(`\n Summary: ${summaryParts.join(', ')}.`);
|
|
571
657
|
}
|
|
572
658
|
export const syncCommand = new Command('sync')
|
|
573
659
|
.description('Sync skills bidirectionally and refresh MCP configs from workspace')
|