@pellux/goodvibes-agent 1.8.1 → 1.9.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 (89) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/dist/package/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-44ar68xs.node} +0 -0
  4. package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-asvc2tv1.node} +0 -0
  5. package/dist/package/main.js +135337 -118733
  6. package/dist/package/{web-tree-sitter-vmqc5d26.wasm → web-tree-sitter-jbz042ba.wasm} +0 -0
  7. package/docs/README.md +1 -1
  8. package/docs/tools-and-commands.md +9 -0
  9. package/package.json +5 -3
  10. package/src/agent/capability-registry.ts +300 -0
  11. package/src/agent/email/style-reply-lane.ts +13 -4
  12. package/src/agent/memory-consolidation-receipts.ts +79 -0
  13. package/src/agent/memory-prompt.ts +3 -1
  14. package/src/agent/operator-gateway-call.ts +134 -0
  15. package/src/agent/principal-attribution.ts +97 -0
  16. package/src/agent/prompt-context-receipts.ts +5 -1
  17. package/src/agent/unified-inbox.ts +17 -1
  18. package/src/cli/bundle-command.ts +2 -1
  19. package/src/cli/channel-profiles-command.ts +168 -0
  20. package/src/cli/ci-command.ts +214 -0
  21. package/src/cli/completion.ts +5 -0
  22. package/src/cli/connected-host-metrics.ts +165 -0
  23. package/src/cli/entrypoint.ts +25 -1
  24. package/src/cli/fleet-command.ts +178 -0
  25. package/src/cli/help.ts +115 -0
  26. package/src/cli/import-command.ts +208 -0
  27. package/src/cli/index.ts +1 -0
  28. package/src/cli/management-commands.ts +9 -8
  29. package/src/cli/management.ts +42 -0
  30. package/src/cli/memory-command.ts +4 -3
  31. package/src/cli/openclaw-import.ts +382 -0
  32. package/src/cli/operator-command-args.ts +88 -0
  33. package/src/cli/parser.ts +12 -0
  34. package/src/cli/personas-command.ts +3 -2
  35. package/src/cli/principals-command.ts +230 -0
  36. package/src/cli/profiles-command.ts +2 -1
  37. package/src/cli/relay-command.ts +100 -0
  38. package/src/cli/routines-command.ts +4 -3
  39. package/src/cli/skill-bundle-command.ts +3 -2
  40. package/src/cli/skills-command.ts +3 -2
  41. package/src/cli/status.ts +114 -2
  42. package/src/cli/temporal-label.ts +89 -0
  43. package/src/cli/types.ts +7 -0
  44. package/src/cli/workspaces-command.ts +135 -0
  45. package/src/config/agent-settings-policy.ts +8 -0
  46. package/src/config/checkpoint-settings.ts +113 -0
  47. package/src/config/workspace-registration.ts +299 -0
  48. package/src/core/rewind-turn-anchors.ts +82 -0
  49. package/src/input/agent-workspace-categories.ts +1 -0
  50. package/src/input/agent-workspace-channel-triage.ts +27 -8
  51. package/src/input/agent-workspace-local-library-snapshot.ts +5 -1
  52. package/src/input/agent-workspace-onboarding-categories.ts +17 -1
  53. package/src/input/agent-workspace-snapshot.ts +25 -3
  54. package/src/input/agent-workspace-types.ts +5 -5
  55. package/src/input/commands/connected-host-admin-runtime.ts +75 -0
  56. package/src/input/commands.ts +2 -0
  57. package/src/input/settings-modal-types.ts +16 -5
  58. package/src/main.ts +17 -12
  59. package/src/permissions/approval-posture.ts +40 -5
  60. package/src/permissions/prompt.ts +35 -1
  61. package/src/renderer/settings-modal-helpers.ts +11 -0
  62. package/src/renderer/settings-modal.ts +21 -0
  63. package/src/renderer/terminal-escapes.ts +65 -12
  64. package/src/runtime/agent-runtime-events.ts +102 -3
  65. package/src/runtime/bootstrap-core.ts +111 -10
  66. package/src/runtime/bootstrap-external-services.ts +156 -0
  67. package/src/runtime/bootstrap.ts +73 -69
  68. package/src/runtime/context-accounting-source.ts +114 -0
  69. package/src/runtime/conversation-rewind-port.ts +133 -0
  70. package/src/runtime/index.ts +8 -28
  71. package/src/runtime/memory-consolidation-scheduler.ts +67 -0
  72. package/src/runtime/memory-consolidation-wiring.ts +56 -0
  73. package/src/runtime/memory-usage-wiring.ts +75 -0
  74. package/src/runtime/services.ts +250 -14
  75. package/src/runtime/tool-permission-safety.ts +18 -1
  76. package/src/shell/blocking-input.ts +38 -8
  77. package/src/shell/startup-wiring.ts +26 -1
  78. package/src/tools/agent-context-policy.ts +1 -1
  79. package/src/tools/agent-harness-cli-command-policy.ts +29 -3
  80. package/src/tools/agent-harness-learning-auto-promote.ts +7 -3
  81. package/src/tools/agent-harness-memory-posture.ts +57 -2
  82. package/src/tools/agent-harness-metadata.ts +12 -0
  83. package/src/tools/agent-harness-mode-catalog.ts +1 -1
  84. package/src/tools/agent-harness-personal-ops-lanes.ts +8 -6
  85. package/src/tools/agent-harness-prompt-context.ts +3 -1
  86. package/src/tools/agent-harness-tool.ts +3 -2
  87. package/src/tools/agent-policy-explanation.ts +40 -0
  88. package/src/tools/tool-definition-compaction.ts +2 -0
  89. package/src/version.ts +1 -1
@@ -0,0 +1,382 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { parseMarkdownFrontmatter, stripMarkdownFrontmatter } from '../agent/markdown-frontmatter.ts';
4
+
5
+ /**
6
+ * OpenClaw workspace importer — pure scanner.
7
+ *
8
+ * Targets OpenClaw workspace layout v1 (the documented/observable on-disk
9
+ * layout; this importer never installs or runs OpenClaw itself). A workspace
10
+ * is a directory (default ~/.openclaw, or a user-supplied path) laid out as:
11
+ *
12
+ * <root>/
13
+ * CLAUDE.md | AGENTS.md | OPENCLAW.md top-level instruction files -> personas
14
+ * instructions/*.md | personas/*.md instruction files -> personas
15
+ * memory.md | memory/*.md memory markdown -> memory records
16
+ * skills/<name>.md | skills/<name>/SKILL.md skill definitions -> skills
17
+ * allowed-tools.json | allowlist.json | tool allowlist config -> permission settings
18
+ * tools.json | permissions.json
19
+ *
20
+ * Frontmatter conventions honored (all optional):
21
+ * persona: name, description (or summary/title)
22
+ * memory: class, scope, tags (comma-separated), summary
23
+ * skill: name, description (or summary), requires-env, requires-command(s)
24
+ *
25
+ * This module only READS. It produces an OpenClawImportPlan; writing through
26
+ * the Agent's registries happens in import-command.ts. Anything that is not a
27
+ * recognized instruction/memory/skill/allowlist file is reported as skipped
28
+ * with a plain reason — never guessed.
29
+ */
30
+
31
+ export const OPENCLAW_LAYOUT_VERSION = 'v1';
32
+
33
+ const VALID_MEMORY_CLASSES = new Set([
34
+ 'decision', 'constraint', 'incident', 'pattern', 'fact', 'risk', 'runbook', 'architecture', 'ownership',
35
+ ]);
36
+ const VALID_MEMORY_SCOPES = new Set(['session', 'project', 'team']);
37
+
38
+ const INSTRUCTION_FILENAMES = new Set(['claude.md', 'agents.md', 'openclaw.md']);
39
+ const INSTRUCTION_DIRS = ['instructions', 'personas'];
40
+ const ALLOWLIST_FILENAMES = ['allowed-tools.json', 'allowlist.json', 'tools.json', 'permissions.json'];
41
+
42
+ /**
43
+ * OpenClaw allowlist tool identifier -> Agent permission tool category. The
44
+ * Agent permission model groups tools into categories (see the SDK config
45
+ * schema permissions.tools). An allowlist entry maps to a category by its
46
+ * leading verb; `Bash(git*)`-style entries map by the tool name before `(`.
47
+ */
48
+ const TOOL_CATEGORY_MAP: Readonly<Record<string, string>> = {
49
+ read: 'read', view: 'read', cat: 'read', open: 'read',
50
+ write: 'write', create: 'write',
51
+ edit: 'edit', str_replace: 'edit', patch: 'edit', apply_patch: 'edit',
52
+ exec: 'exec', bash: 'exec', shell: 'exec', run: 'exec', terminal: 'exec', command: 'exec',
53
+ find: 'find', grep: 'find', search: 'find', glob: 'find', ls: 'find',
54
+ fetch: 'fetch', web: 'fetch', webfetch: 'fetch', web_fetch: 'fetch', curl: 'fetch', http: 'fetch', browse: 'fetch',
55
+ analyze: 'analyze', analysis: 'analyze',
56
+ inspect: 'inspect',
57
+ agent: 'agent', task: 'agent', subagent: 'agent', dispatch: 'agent',
58
+ state: 'state', memory: 'state', store: 'state',
59
+ workflow: 'workflow',
60
+ registry: 'registry',
61
+ delegate: 'delegate',
62
+ mcp: 'mcp',
63
+ };
64
+
65
+ export interface PlannedPersona {
66
+ readonly name: string;
67
+ readonly description: string;
68
+ readonly body: string;
69
+ readonly tags: readonly string[];
70
+ readonly sourcePath: string;
71
+ }
72
+
73
+ export interface PlannedMemory {
74
+ readonly cls: string;
75
+ readonly scope: string;
76
+ readonly summary: string;
77
+ readonly detail?: string;
78
+ readonly tags: readonly string[];
79
+ readonly sourcePath: string;
80
+ }
81
+
82
+ export interface PlannedSkill {
83
+ readonly name: string;
84
+ readonly description: string;
85
+ readonly procedure: string;
86
+ readonly tags: readonly string[];
87
+ readonly requiresEnv: readonly string[];
88
+ readonly requiresCommand: readonly string[];
89
+ readonly sourcePath: string;
90
+ }
91
+
92
+ export interface PlannedPermissions {
93
+ readonly sourcePath: string | null;
94
+ /** Distinct Agent tool categories that would be set to 'allow'. */
95
+ readonly categories: readonly string[];
96
+ /** Allowlist entries that mapped to a category, for the report. */
97
+ readonly matched: readonly { readonly entry: string; readonly category: string }[];
98
+ }
99
+
100
+ export interface SkippedEntry {
101
+ readonly path: string;
102
+ readonly reason: string;
103
+ }
104
+
105
+ export interface OpenClawImportPlan {
106
+ readonly sourcePath: string;
107
+ readonly exists: boolean;
108
+ readonly personas: readonly PlannedPersona[];
109
+ readonly memories: readonly PlannedMemory[];
110
+ readonly skills: readonly PlannedSkill[];
111
+ readonly permissions: PlannedPermissions;
112
+ readonly skipped: readonly SkippedEntry[];
113
+ }
114
+
115
+ function frontmatterList(frontmatter: Record<string, string>, ...keys: readonly string[]): readonly string[] {
116
+ for (const key of keys) {
117
+ const value = frontmatter[key];
118
+ if (value) return value.split(',').map((entry) => entry.trim()).filter(Boolean);
119
+ }
120
+ return [];
121
+ }
122
+
123
+ function firstFrontmatter(frontmatter: Record<string, string>, ...keys: readonly string[]): string | undefined {
124
+ for (const key of keys) {
125
+ const value = frontmatter[key]?.trim();
126
+ if (value) return value;
127
+ }
128
+ return undefined;
129
+ }
130
+
131
+ function safeReadFile(path: string): string | null {
132
+ try {
133
+ return readFileSync(path, 'utf-8');
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ function listDir(path: string): readonly string[] {
140
+ try {
141
+ return readdirSync(path).sort((a, b) => a.localeCompare(b));
142
+ } catch {
143
+ return [];
144
+ }
145
+ }
146
+
147
+ function isDirectory(path: string): boolean {
148
+ try {
149
+ return statSync(path).isDirectory();
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ function personaFromFile(path: string, skipped: SkippedEntry[]): PlannedPersona | null {
156
+ const content = safeReadFile(path);
157
+ if (content === null) {
158
+ skipped.push({ path, reason: 'could not read instruction file' });
159
+ return null;
160
+ }
161
+ const frontmatter = parseMarkdownFrontmatter(content);
162
+ const body = stripMarkdownFrontmatter(content);
163
+ if (!body) {
164
+ skipped.push({ path, reason: 'instruction file has no body after frontmatter' });
165
+ return null;
166
+ }
167
+ const name = firstFrontmatter(frontmatter, 'name') ?? basename(path).replace(/\.md$/i, '');
168
+ const description = firstFrontmatter(frontmatter, 'description', 'summary', 'title')
169
+ ?? `Imported from OpenClaw instruction file ${basename(path)}.`;
170
+ return { name, description, body, tags: frontmatterList(frontmatter, 'tags'), sourcePath: path };
171
+ }
172
+
173
+ function collectPersonas(root: string, skipped: SkippedEntry[]): PlannedPersona[] {
174
+ const personas: PlannedPersona[] = [];
175
+ for (const entry of listDir(root)) {
176
+ if (!entry.toLowerCase().endsWith('.md')) continue;
177
+ const full = join(root, entry);
178
+ if (isDirectory(full)) continue;
179
+ if (INSTRUCTION_FILENAMES.has(entry.toLowerCase())) {
180
+ const persona = personaFromFile(full, skipped);
181
+ if (persona) personas.push(persona);
182
+ } else {
183
+ skipped.push({ path: full, reason: 'top-level markdown is not a recognized instruction file (CLAUDE.md, AGENTS.md, OPENCLAW.md)' });
184
+ }
185
+ }
186
+ for (const dir of INSTRUCTION_DIRS) {
187
+ const dirPath = join(root, dir);
188
+ if (!isDirectory(dirPath)) continue;
189
+ for (const entry of listDir(dirPath)) {
190
+ if (!entry.toLowerCase().endsWith('.md')) continue;
191
+ const persona = personaFromFile(join(dirPath, entry), skipped);
192
+ if (persona) personas.push(persona);
193
+ }
194
+ }
195
+ return personas;
196
+ }
197
+
198
+ function memoriesFromFile(path: string, skipped: SkippedEntry[]): PlannedMemory[] {
199
+ const content = safeReadFile(path);
200
+ if (content === null) {
201
+ skipped.push({ path, reason: 'could not read memory file' });
202
+ return [];
203
+ }
204
+ const frontmatter = parseMarkdownFrontmatter(content);
205
+ const clsRaw = firstFrontmatter(frontmatter, 'class', 'cls');
206
+ if (clsRaw && !VALID_MEMORY_CLASSES.has(clsRaw)) {
207
+ skipped.push({ path, reason: `unknown memory class "${clsRaw}" (expected one of ${[...VALID_MEMORY_CLASSES].join(', ')})` });
208
+ return [];
209
+ }
210
+ const scopeRaw = firstFrontmatter(frontmatter, 'scope');
211
+ if (scopeRaw && !VALID_MEMORY_SCOPES.has(scopeRaw)) {
212
+ skipped.push({ path, reason: `unknown memory scope "${scopeRaw}" (expected one of ${[...VALID_MEMORY_SCOPES].join(', ')})` });
213
+ return [];
214
+ }
215
+ const cls = clsRaw ?? 'fact';
216
+ const scope = scopeRaw ?? 'project';
217
+ const tags = frontmatterList(frontmatter, 'tags');
218
+ const body = stripMarkdownFrontmatter(content);
219
+ const bullets = body
220
+ .split('\n')
221
+ .map((line) => line.trim())
222
+ .filter((line) => /^[-*]\s+/.test(line))
223
+ .map((line) => line.replace(/^[-*]\s+/, '').trim())
224
+ .filter(Boolean);
225
+ if (bullets.length > 0) {
226
+ return bullets.map((summary) => ({ cls, scope, summary, tags, sourcePath: path }));
227
+ }
228
+ const lines = body.split('\n').map((line) => line.trim());
229
+ const summary = lines.find(Boolean)?.replace(/^#+\s*/, '') ?? '';
230
+ if (!summary) {
231
+ skipped.push({ path, reason: 'memory file has no bullet points or text after frontmatter' });
232
+ return [];
233
+ }
234
+ const detail = lines.slice(lines.indexOf(lines.find(Boolean) ?? '') + 1).join('\n').trim() || undefined;
235
+ return [{ cls, scope, summary, detail, tags, sourcePath: path }];
236
+ }
237
+
238
+ function collectMemories(root: string, skipped: SkippedEntry[]): PlannedMemory[] {
239
+ const memories: PlannedMemory[] = [];
240
+ const topLevel = join(root, 'memory.md');
241
+ if (existsSync(topLevel) && !isDirectory(topLevel)) {
242
+ memories.push(...memoriesFromFile(topLevel, skipped));
243
+ }
244
+ const dirPath = join(root, 'memory');
245
+ if (isDirectory(dirPath)) {
246
+ for (const entry of listDir(dirPath)) {
247
+ if (!entry.toLowerCase().endsWith('.md')) {
248
+ skipped.push({ path: join(dirPath, entry), reason: 'non-markdown file in memory folder' });
249
+ continue;
250
+ }
251
+ memories.push(...memoriesFromFile(join(dirPath, entry), skipped));
252
+ }
253
+ }
254
+ return memories;
255
+ }
256
+
257
+ function skillFromFile(path: string, nameHint: string, skipped: SkippedEntry[]): PlannedSkill | null {
258
+ const content = safeReadFile(path);
259
+ if (content === null) {
260
+ skipped.push({ path, reason: 'could not read skill file' });
261
+ return null;
262
+ }
263
+ const frontmatter = parseMarkdownFrontmatter(content);
264
+ const procedure = stripMarkdownFrontmatter(content);
265
+ if (!procedure) {
266
+ skipped.push({ path, reason: 'skill file has no procedure body after frontmatter' });
267
+ return null;
268
+ }
269
+ const name = firstFrontmatter(frontmatter, 'name') ?? nameHint;
270
+ const description = firstFrontmatter(frontmatter, 'description', 'summary')
271
+ ?? `Imported from OpenClaw skill file ${basename(path)}.`;
272
+ return {
273
+ name,
274
+ description,
275
+ procedure,
276
+ tags: frontmatterList(frontmatter, 'tags'),
277
+ requiresEnv: frontmatterList(frontmatter, 'requires-env', 'requiresEnv', 'requires_env'),
278
+ requiresCommand: frontmatterList(frontmatter, 'requires-command', 'requires-commands', 'requiresCommands', 'commands'),
279
+ sourcePath: path,
280
+ };
281
+ }
282
+
283
+ function collectSkills(root: string, skipped: SkippedEntry[]): PlannedSkill[] {
284
+ const dirPath = join(root, 'skills');
285
+ if (!isDirectory(dirPath)) return [];
286
+ const skills: PlannedSkill[] = [];
287
+ for (const entry of listDir(dirPath)) {
288
+ const full = join(dirPath, entry);
289
+ if (isDirectory(full)) {
290
+ const markerCandidates = ['SKILL.md', 'skill.md'];
291
+ const marker = markerCandidates.map((m) => join(full, m)).find((p) => existsSync(p));
292
+ if (marker) {
293
+ const skill = skillFromFile(marker, entry, skipped);
294
+ if (skill) skills.push(skill);
295
+ } else {
296
+ skipped.push({ path: full, reason: 'skill folder has no SKILL.md' });
297
+ }
298
+ continue;
299
+ }
300
+ if (!entry.toLowerCase().endsWith('.md')) {
301
+ skipped.push({ path: full, reason: 'non-markdown file in skills folder' });
302
+ continue;
303
+ }
304
+ const skill = skillFromFile(full, entry.replace(/\.md$/i, ''), skipped);
305
+ if (skill) skills.push(skill);
306
+ }
307
+ return skills;
308
+ }
309
+
310
+ /** Normalize one allowlist entry to a bare tool name (strips `Bash(git*)` args and casing). */
311
+ function normalizeAllowlistEntry(entry: string): string {
312
+ const beforeParen = entry.split('(')[0] ?? entry;
313
+ return beforeParen.trim().toLowerCase().replace(/[\s-]+/g, '_');
314
+ }
315
+
316
+ function collectAllowlistEntries(value: unknown): readonly string[] {
317
+ if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === 'string');
318
+ if (value && typeof value === 'object') {
319
+ const record = value as Record<string, unknown>;
320
+ for (const key of ['allow', 'allowed', 'allowedTools', 'allowed_tools', 'tools']) {
321
+ const nested = record[key];
322
+ if (Array.isArray(nested)) return nested.filter((entry): entry is string => typeof entry === 'string');
323
+ }
324
+ }
325
+ return [];
326
+ }
327
+
328
+ function collectPermissions(root: string, skipped: SkippedEntry[]): PlannedPermissions {
329
+ for (const filename of ALLOWLIST_FILENAMES) {
330
+ const full = join(root, filename);
331
+ if (!existsSync(full) || isDirectory(full)) continue;
332
+ const content = safeReadFile(full);
333
+ if (content === null) {
334
+ skipped.push({ path: full, reason: 'could not read allowlist file' });
335
+ continue;
336
+ }
337
+ let parsed: unknown;
338
+ try {
339
+ parsed = JSON.parse(content);
340
+ } catch {
341
+ skipped.push({ path: full, reason: 'allowlist file is not valid JSON' });
342
+ continue;
343
+ }
344
+ const entries = collectAllowlistEntries(parsed);
345
+ if (entries.length === 0) {
346
+ skipped.push({ path: full, reason: 'allowlist file has no recognizable tool entries (expected an array, or an "allow"/"tools" array)' });
347
+ continue;
348
+ }
349
+ const matched: { entry: string; category: string }[] = [];
350
+ const categories = new Set<string>();
351
+ for (const entry of entries) {
352
+ const category = TOOL_CATEGORY_MAP[normalizeAllowlistEntry(entry)];
353
+ if (category) {
354
+ matched.push({ entry, category });
355
+ categories.add(category);
356
+ } else {
357
+ skipped.push({ path: full, reason: `allowlist entry "${entry}" does not map to a known Agent tool category` });
358
+ }
359
+ }
360
+ return { sourcePath: full, categories: [...categories].sort(), matched };
361
+ }
362
+ return { sourcePath: null, categories: [], matched: [] };
363
+ }
364
+
365
+ export function scanOpenClawWorkspace(sourcePath: string): OpenClawImportPlan {
366
+ const empty: OpenClawImportPlan = {
367
+ sourcePath,
368
+ exists: false,
369
+ personas: [],
370
+ memories: [],
371
+ skills: [],
372
+ permissions: { sourcePath: null, categories: [], matched: [] },
373
+ skipped: [],
374
+ };
375
+ if (!existsSync(sourcePath) || !isDirectory(sourcePath)) return empty;
376
+ const skipped: SkippedEntry[] = [];
377
+ const personas = collectPersonas(sourcePath, skipped);
378
+ const memories = collectMemories(sourcePath, skipped);
379
+ const skills = collectSkills(sourcePath, skipped);
380
+ const permissions = collectPermissions(sourcePath, skipped);
381
+ return { sourcePath, exists: true, personas, memories, skills, permissions, skipped };
382
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Shared flag/positional parser for the connected-host operator CLI commands
3
+ * (ci, principals, channel-profiles). Mirrors parseRoutineOptions in
4
+ * routines-command.ts: `--flag value`, `--flag=value`, bare `--flag` booleans,
5
+ * and a `--yes` confirmation flag, with `--` starting pure positionals.
6
+ */
7
+
8
+ export interface ParsedOperatorCommandArgs {
9
+ readonly positionals: readonly string[];
10
+ readonly flags: ReadonlyMap<string, string>;
11
+ readonly booleanFlags: ReadonlySet<string>;
12
+ readonly yes: boolean;
13
+ }
14
+
15
+ export function parseOperatorCommandArgs(
16
+ args: readonly string[],
17
+ valueFlags: readonly string[] = [],
18
+ ): ParsedOperatorCommandArgs {
19
+ const valued = new Set(valueFlags);
20
+ const flags = new Map<string, string>();
21
+ const booleanFlags = new Set<string>();
22
+ const positionals: string[] = [];
23
+ let yes = false;
24
+ for (let index = 0; index < args.length; index += 1) {
25
+ const token = args[index] ?? '';
26
+ if (token === '--') {
27
+ positionals.push(...args.slice(index + 1));
28
+ break;
29
+ }
30
+ if (token === '--yes') {
31
+ yes = true;
32
+ continue;
33
+ }
34
+ if (token.startsWith('--') && token.length > 2) {
35
+ const raw = token.slice(2);
36
+ const equalIndex = raw.indexOf('=');
37
+ if (equalIndex >= 0) {
38
+ flags.set(raw.slice(0, equalIndex), raw.slice(equalIndex + 1));
39
+ continue;
40
+ }
41
+ const key = raw;
42
+ const next = args[index + 1];
43
+ if (next !== undefined && (valued.has(key) || !next.startsWith('--'))) {
44
+ flags.set(key, next);
45
+ index += 1;
46
+ } else {
47
+ booleanFlags.add(key);
48
+ flags.set(key, 'true');
49
+ }
50
+ continue;
51
+ }
52
+ positionals.push(token);
53
+ }
54
+ return { positionals, flags, booleanFlags, yes };
55
+ }
56
+
57
+ export function operatorFlagValue(parsed: ParsedOperatorCommandArgs, key: string): string | undefined {
58
+ const value = parsed.flags.get(key)?.trim();
59
+ return value ? value : undefined;
60
+ }
61
+
62
+ export function operatorHasFlag(parsed: ParsedOperatorCommandArgs, key: string): boolean {
63
+ return parsed.booleanFlags.has(key) || parsed.flags.get(key) === 'true';
64
+ }
65
+
66
+ export function operatorRequiredFlag(parsed: ParsedOperatorCommandArgs, key: string, usage: string): string {
67
+ const value = operatorFlagValue(parsed, key);
68
+ if (!value) throw new Error(`${usage}\nMissing --${key}.`);
69
+ return value;
70
+ }
71
+
72
+ export function operatorParseIntFlag(parsed: ParsedOperatorCommandArgs, key: string, usage: string): number | undefined {
73
+ const value = operatorFlagValue(parsed, key);
74
+ if (value === undefined) return undefined;
75
+ const parsedNumber = Number.parseInt(value, 10);
76
+ if (!Number.isFinite(parsedNumber)) throw new Error(`${usage}\n--${key} must be a number.`);
77
+ return parsedNumber;
78
+ }
79
+
80
+ /** Splits comma-separated `channel:value` pairs, e.g. "slack:U123,email:a@b.com". */
81
+ export function parseIdentityPairs(raw: string | undefined): readonly { readonly channel: string; readonly value: string }[] {
82
+ if (!raw) return [];
83
+ return raw.split(',').map((entry) => entry.trim()).filter(Boolean).map((entry) => {
84
+ const index = entry.indexOf(':');
85
+ if (index < 0) throw new Error(`Invalid --identity entry "${entry}"; expected channel:value.`);
86
+ return { channel: entry.slice(0, index).trim(), value: entry.slice(index + 1).trim() };
87
+ });
88
+ }
package/src/cli/parser.ts CHANGED
@@ -28,6 +28,16 @@ const COMMAND_ALIASES: Readonly<Record<string, GoodVibesCliCommand>> = {
28
28
  memories: 'memory',
29
29
  routines: 'routines',
30
30
  routine: 'routines',
31
+ ci: 'ci',
32
+ principals: 'principals',
33
+ principal: 'principals',
34
+ 'channel-profiles': 'channel-profiles',
35
+ 'channel-profile': 'channel-profiles',
36
+ workspaces: 'workspaces',
37
+ workspace: 'workspaces',
38
+ relay: 'relay',
39
+ fleet: 'fleet',
40
+ fleets: 'fleet',
31
41
  auth: 'auth',
32
42
  compat: 'compat',
33
43
  compatibility: 'compat',
@@ -52,6 +62,8 @@ const COMMAND_ALIASES: Readonly<Record<string, GoodVibesCliCommand>> = {
52
62
  qr: 'pair',
53
63
  bundle: 'bundle',
54
64
  bundles: 'bundle',
65
+ import: 'import',
66
+ migrate: 'import',
55
67
  completion: 'completion',
56
68
  completions: 'completion',
57
69
  help: 'help',
@@ -3,6 +3,7 @@ import type { AgentPersonaRecord } from '../agent/persona-registry.ts';
3
3
  import { formatAgentRecordOrigin, formatAgentRecordReviewState } from '../agent/record-labels.ts';
4
4
  import type { CliCommandOutput } from './types.ts';
5
5
  import type { CliCommandRuntime } from './management.ts';
6
+ import { appendTemporalLabel } from './temporal-label.ts';
6
7
  import {
7
8
  csvOption,
8
9
  errorOutput,
@@ -45,8 +46,8 @@ function renderPersona(persona: AgentPersonaRecord, activePersonaId: string | nu
45
46
  ` origin: ${formatAgentRecordOrigin(persona.source, persona.provenance)}`,
46
47
  ` tags: ${persona.tags.join(', ') || '(none)'}`,
47
48
  ` triggers: ${persona.triggers.join(', ') || '(manual)'}`,
48
- ` created: ${persona.createdAt}`,
49
- ` updated: ${persona.updatedAt}`,
49
+ ` created: ${appendTemporalLabel(persona.createdAt, persona.createdAt)}`,
50
+ ` updated: ${appendTemporalLabel(persona.updatedAt, persona.updatedAt)}`,
50
51
  persona.staleReason ? ` stale reason: ${persona.staleReason}` : '',
51
52
  '',
52
53
  persona.description,