runwork 0.12.0 → 0.13.2
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/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/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/doctor.js +89 -2
- package/dist/commands/inbox.d.ts +11 -0
- package/dist/commands/inbox.js +60 -0
- package/dist/commands/resume.d.ts +2 -0
- package/dist/commands/resume.js +289 -0
- package/dist/commands/save-convo.d.ts +8 -0
- package/dist/commands/save-convo.js +20 -0
- package/dist/commands/share-convo.d.ts +24 -0
- package/dist/commands/share-convo.js +167 -0
- package/dist/commands/sync.js +95 -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/package.json +1 -1
|
@@ -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) {
|
|
@@ -306,7 +323,28 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
306
323
|
}
|
|
307
324
|
catch { /* ignore */ }
|
|
308
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
|
+
};
|
|
309
346
|
for (const adapter of adapters) {
|
|
347
|
+
let adapterFailedAnyScope = false;
|
|
310
348
|
for (const scope of scopes) {
|
|
311
349
|
try {
|
|
312
350
|
// Write skills: for project scope, filter to only the current app's skill.
|
|
@@ -316,6 +354,11 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
316
354
|
if (adapter.supportsSkills()) {
|
|
317
355
|
const skipAppSkills = adapter.mcpProvidesSkills && mcpEntries.length > 0;
|
|
318
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;
|
|
319
362
|
// App-sourced skills are already available via MCP skill_* tools
|
|
320
363
|
if (skipAppSkills && s.source === 'app')
|
|
321
364
|
return false;
|
|
@@ -325,29 +368,57 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
325
368
|
}
|
|
326
369
|
return true;
|
|
327
370
|
});
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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];
|
|
336
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`);
|
|
337
389
|
}
|
|
338
390
|
// Write MCP configs
|
|
339
391
|
if (adapter.supportsMcpScope(scope) && mcpEntries.length > 0) {
|
|
340
392
|
await adapter.writeMcpServers(mcpEntries, scope);
|
|
393
|
+
summary.mcpServerWrites++;
|
|
341
394
|
console.log(` [${adapter.name}] Updated ${mcpEntries.length} MCP server${mcpEntries.length > 1 ? 's' : ''} (${scope})`);
|
|
342
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
|
+
}
|
|
343
402
|
// Write instruction hint
|
|
344
403
|
await adapter.writeInstructionHint(instructionHint, scope);
|
|
404
|
+
summary.instructionHintWrites++;
|
|
345
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
|
+
}
|
|
346
413
|
}
|
|
347
414
|
catch (err) {
|
|
415
|
+
adapterFailedAnyScope = true;
|
|
348
416
|
console.warn(` [${adapter.name}] Failed (${scope}): ${err instanceof Error ? err.message : err}`);
|
|
349
417
|
}
|
|
350
418
|
}
|
|
419
|
+
summary.adaptersProcessed++;
|
|
420
|
+
if (adapterFailedAnyScope)
|
|
421
|
+
summary.adaptersFailed++;
|
|
351
422
|
}
|
|
352
423
|
// Pull team config from server. A failure here must not stop the
|
|
353
424
|
// unified user-scope write below, which applies network and minimum-permission
|
|
@@ -569,6 +640,20 @@ export async function syncFromState(state, statePath, credentials, opts) {
|
|
|
569
640
|
catch {
|
|
570
641
|
// Telemetry failures are non-fatal
|
|
571
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(', ')}.`);
|
|
572
657
|
}
|
|
573
658
|
export const syncCommand = new Command('sync')
|
|
574
659
|
.description('Sync skills bidirectionally and refresh MCP configs from workspace')
|