@pellux/goodvibes-agent 0.1.113 → 0.1.116
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/CHANGELOG.md +19 -0
- package/README.md +4 -0
- package/dist/package/main.js +1659 -324
- package/docs/getting-started.md +4 -0
- package/package.json +1 -1
- package/src/agent/behavior-discovery-summary.ts +167 -0
- package/src/agent/persona-discovery.ts +117 -0
- package/src/agent/routine-discovery.ts +115 -0
- package/src/agent/runtime-profile-starters.ts +331 -0
- package/src/agent/runtime-profile.ts +150 -330
- package/src/cli/help.ts +11 -3
- package/src/cli/local-library-command.ts +81 -1
- package/src/cli/profiles-command.ts +128 -0
- package/src/cli/routines-command.ts +156 -1
- package/src/input/agent-workspace-basic-command-editor-submission.ts +534 -0
- package/src/input/agent-workspace-basic-command-editors.ts +77 -395
- package/src/input/agent-workspace-categories.ts +7 -0
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-setup.ts +33 -13
- package/src/input/agent-workspace-snapshot.ts +6 -0
- package/src/input/agent-workspace-types.ts +6 -0
- package/src/input/commands/agent-runtime-profile-runtime.ts +73 -4
- package/src/input/commands/personas-runtime.ts +87 -2
- package/src/input/commands/routines-runtime.ts +87 -2
- package/src/input/onboarding/onboarding-wizard-operator-steps.ts +46 -9
- package/src/input/onboarding/onboarding-wizard-steps.ts +1 -1
- package/src/renderer/agent-workspace.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +20 -1
- package/src/runtime/onboarding/snapshot.ts +2 -0
- package/src/runtime/onboarding/types.ts +2 -0
- package/src/version.ts +1 -1
package/docs/getting-started.md
CHANGED
|
@@ -46,8 +46,12 @@ bun run dev
|
|
|
46
46
|
|
|
47
47
|
After setup has been shown once, the TUI opens directly into the Agent operator workspace. You can also reopen it with `/agent`, `/home`, or `/operator`. That fullscreen workspace is the current front door for setup/config, provider/model selection, Agent Knowledge, local memory/skills/routines/personas, channel readiness, voice/media setup, read-only work/approval/automation views, and explicit GoodVibes TUI build delegation.
|
|
48
48
|
|
|
49
|
+
The setup workspace scans local Agent behavior folders and shows importable persona, skill, and routine files before asking you to create blank records. Use `/personas discover`, `/agent-skills discover`, and `/routines discover` to preview files, then use the matching import action in the workspace after review.
|
|
50
|
+
|
|
49
51
|
Use `/agent-profile guide` inside that workspace to walk through starter-profile authoring. It lists built-in and local starters, exports a JSON starter for editing, imports the edited starter back into this Agent home, and creates isolated profiles from the result.
|
|
50
52
|
|
|
53
|
+
Use `goodvibes-agent profiles create-from-discovered research-desk --yes` or the Profiles workspace form to assemble a local starter template and isolated Agent profile from reviewed discovered persona, skill, and routine files. Use `profiles templates from-discovered <id> --yes` only when you want to save the starter before creating a profile.
|
|
54
|
+
|
|
51
55
|
Use the Knowledge area in that workspace to ingest a source URL without leaving the TUI. The form requires typed confirmation and writes only to the isolated Agent Knowledge segment.
|
|
52
56
|
|
|
53
57
|
Use `/schedule receipts` to review redacted local routine promotion history and `/schedule reconcile` to compare those receipts with live connected schedules through public `schedules.list`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.116",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
|
|
6
|
+
type DiscoveryOrigin = 'project-local' | 'global';
|
|
7
|
+
|
|
8
|
+
interface DiscoveryRoot {
|
|
9
|
+
readonly root: string;
|
|
10
|
+
readonly origin: DiscoveryOrigin;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface DiscoveryKindDefinition {
|
|
14
|
+
readonly roots: readonly DiscoveryRoot[];
|
|
15
|
+
readonly markers: readonly string[];
|
|
16
|
+
readonly frontmatterBodyKey?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AgentBehaviorDiscoverySummary {
|
|
20
|
+
readonly count: number;
|
|
21
|
+
readonly projectLocalCount: number;
|
|
22
|
+
readonly globalCount: number;
|
|
23
|
+
readonly names: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AgentBehaviorDiscoverySnapshot {
|
|
27
|
+
readonly personas: AgentBehaviorDiscoverySummary;
|
|
28
|
+
readonly skills: AgentBehaviorDiscoverySummary;
|
|
29
|
+
readonly routines: AgentBehaviorDiscoverySummary;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const EMPTY_SUMMARY: AgentBehaviorDiscoverySummary = {
|
|
33
|
+
count: 0,
|
|
34
|
+
projectLocalCount: 0,
|
|
35
|
+
globalCount: 0,
|
|
36
|
+
names: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const EMPTY_AGENT_BEHAVIOR_DISCOVERY_SNAPSHOT: AgentBehaviorDiscoverySnapshot = {
|
|
40
|
+
personas: EMPTY_SUMMARY,
|
|
41
|
+
skills: EMPTY_SUMMARY,
|
|
42
|
+
routines: EMPTY_SUMMARY,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function parseFrontmatter(content: string): Record<string, string> {
|
|
46
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
47
|
+
if (!match) return {};
|
|
48
|
+
const result: Record<string, string> = {};
|
|
49
|
+
for (const line of match[1].split('\n')) {
|
|
50
|
+
const [key, ...rest] = line.split(':');
|
|
51
|
+
if (key && rest.length > 0) {
|
|
52
|
+
result[key.trim()] = rest.join(':').trim();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function markdownBody(content: string): string {
|
|
59
|
+
return content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readDiscoveryCandidate(path: string, origin: DiscoveryOrigin, definition: DiscoveryKindDefinition): { readonly name: string; readonly origin: DiscoveryOrigin } | null {
|
|
63
|
+
let content = '';
|
|
64
|
+
try {
|
|
65
|
+
content = readFileSync(path, 'utf-8');
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const frontmatter = parseFrontmatter(content);
|
|
71
|
+
const body = definition.frontmatterBodyKey
|
|
72
|
+
? (frontmatter[definition.frontmatterBodyKey] ?? markdownBody(content)).trim()
|
|
73
|
+
: markdownBody(content);
|
|
74
|
+
if (body.length === 0) return null;
|
|
75
|
+
const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
|
|
76
|
+
const normalized = name.trim();
|
|
77
|
+
return normalized ? { name: normalized, origin } : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function candidatePaths(root: string, entry: string, markers: readonly string[]): readonly string[] {
|
|
81
|
+
if (entry.endsWith('.md')) return [join(root, entry)];
|
|
82
|
+
return markers.map((marker) => join(root, entry, marker));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function summarizeDefinition(definition: DiscoveryKindDefinition, limit: number): AgentBehaviorDiscoverySummary {
|
|
86
|
+
const seen = new Set<string>();
|
|
87
|
+
const names: string[] = [];
|
|
88
|
+
let projectLocalCount = 0;
|
|
89
|
+
let globalCount = 0;
|
|
90
|
+
|
|
91
|
+
for (const { root, origin } of definition.roots) {
|
|
92
|
+
let entries: string[] = [];
|
|
93
|
+
try {
|
|
94
|
+
entries = readdirSync(root);
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const entry of entries.sort((left, right) => left.localeCompare(right))) {
|
|
99
|
+
for (const path of candidatePaths(root, entry, definition.markers)) {
|
|
100
|
+
const candidate = readDiscoveryCandidate(path, origin, definition);
|
|
101
|
+
if (!candidate) continue;
|
|
102
|
+
const key = candidate.name.toLowerCase();
|
|
103
|
+
if (seen.has(key)) break;
|
|
104
|
+
seen.add(key);
|
|
105
|
+
if (candidate.origin === 'project-local') projectLocalCount += 1;
|
|
106
|
+
else globalCount += 1;
|
|
107
|
+
if (names.length < limit) names.push(candidate.name);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (seen.size === 0) return EMPTY_SUMMARY;
|
|
114
|
+
return {
|
|
115
|
+
count: seen.size,
|
|
116
|
+
projectLocalCount,
|
|
117
|
+
globalCount,
|
|
118
|
+
names,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function definitions(shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>): AgentBehaviorDiscoverySnapshot {
|
|
123
|
+
const cwd = shellPaths.workingDirectory;
|
|
124
|
+
const homeDir = shellPaths.homeDirectory;
|
|
125
|
+
const personas: DiscoveryKindDefinition = {
|
|
126
|
+
roots: [
|
|
127
|
+
{ root: join(cwd, '.goodvibes', 'personas'), origin: 'project-local' },
|
|
128
|
+
{ root: join(cwd, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'personas'), origin: 'project-local' },
|
|
129
|
+
{ root: join(cwd, '.goodvibes', 'agents'), origin: 'project-local' },
|
|
130
|
+
{ root: join(homeDir, '.goodvibes', 'personas'), origin: 'global' },
|
|
131
|
+
{ root: join(homeDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'personas'), origin: 'global' },
|
|
132
|
+
{ root: join(homeDir, '.goodvibes', 'agents'), origin: 'global' },
|
|
133
|
+
],
|
|
134
|
+
markers: ['PERSONA.md', 'persona.md', 'AGENT.md', 'agent.md'],
|
|
135
|
+
frontmatterBodyKey: 'system_prompt',
|
|
136
|
+
};
|
|
137
|
+
const skills: DiscoveryKindDefinition = {
|
|
138
|
+
roots: [
|
|
139
|
+
{ root: join(cwd, '.goodvibes', 'skills'), origin: 'project-local' },
|
|
140
|
+
{ root: join(cwd, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'skills'), origin: 'project-local' },
|
|
141
|
+
{ root: join(homeDir, '.goodvibes', 'skills'), origin: 'global' },
|
|
142
|
+
{ root: join(homeDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'skills'), origin: 'global' },
|
|
143
|
+
],
|
|
144
|
+
markers: ['SKILL.md'],
|
|
145
|
+
};
|
|
146
|
+
const routines: DiscoveryKindDefinition = {
|
|
147
|
+
roots: [
|
|
148
|
+
{ root: join(cwd, '.goodvibes', 'routines'), origin: 'project-local' },
|
|
149
|
+
{ root: join(cwd, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'routines'), origin: 'project-local' },
|
|
150
|
+
{ root: join(homeDir, '.goodvibes', 'routines'), origin: 'global' },
|
|
151
|
+
{ root: join(homeDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'routines'), origin: 'global' },
|
|
152
|
+
],
|
|
153
|
+
markers: ['ROUTINE.md', 'routine.md'],
|
|
154
|
+
frontmatterBodyKey: 'steps',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
personas: summarizeDefinition(personas, 4),
|
|
159
|
+
skills: summarizeDefinition(skills, 4),
|
|
160
|
+
routines: summarizeDefinition(routines, 4),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function summarizeAgentBehaviorDiscovery(shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'> | undefined): AgentBehaviorDiscoverySnapshot {
|
|
165
|
+
if (!shellPaths) return EMPTY_AGENT_BEHAVIOR_DISCOVERY_SNAPSHOT;
|
|
166
|
+
return definitions(shellPaths);
|
|
167
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { promises as fsPromises } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
|
|
6
|
+
export type PersonaOrigin = 'project-local' | 'global' | 'custom';
|
|
7
|
+
|
|
8
|
+
export interface DiscoveredPersonaRecord {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly origin: PersonaOrigin;
|
|
13
|
+
readonly body: string;
|
|
14
|
+
readonly frontmatter: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DIRECTORY_MARKERS: readonly string[] = ['PERSONA.md', 'persona.md', 'AGENT.md', 'agent.md'];
|
|
18
|
+
|
|
19
|
+
function parseFrontmatter(content: string): Record<string, string> {
|
|
20
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
+
if (!match) return {};
|
|
22
|
+
const result: Record<string, string> = {};
|
|
23
|
+
for (const line of match[1].split('\n')) {
|
|
24
|
+
const [key, ...rest] = line.split(':');
|
|
25
|
+
if (key && rest.length > 0) {
|
|
26
|
+
result[key.trim()] = rest.join(':').trim();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getPersonaDirectories(cwd: string, homeDir: string): Array<{ root: string; origin: PersonaOrigin }> {
|
|
33
|
+
return [
|
|
34
|
+
{ root: join(cwd, '.goodvibes', 'personas'), origin: 'project-local' },
|
|
35
|
+
{ root: join(cwd, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'personas'), origin: 'project-local' },
|
|
36
|
+
{ root: join(cwd, '.goodvibes', 'agents'), origin: 'project-local' },
|
|
37
|
+
{ root: join(homeDir, '.goodvibes', 'personas'), origin: 'global' },
|
|
38
|
+
{ root: join(homeDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'personas'), origin: 'global' },
|
|
39
|
+
{ root: join(homeDir, '.goodvibes', 'agents'), origin: 'global' },
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readPersonaFile(path: string, origin: PersonaOrigin): Promise<DiscoveredPersonaRecord | null> {
|
|
44
|
+
let content = '';
|
|
45
|
+
try {
|
|
46
|
+
content = await fsPromises.readFile(path, 'utf-8');
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const frontmatter = parseFrontmatter(content);
|
|
52
|
+
const markdownBody = content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
53
|
+
const body = (frontmatter.system_prompt ?? markdownBody).trim();
|
|
54
|
+
if (!body) return null;
|
|
55
|
+
const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? 'persona';
|
|
56
|
+
const description = frontmatter.description ?? frontmatter.summary ?? frontmatter.title ?? '';
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
description,
|
|
61
|
+
path,
|
|
62
|
+
origin,
|
|
63
|
+
body,
|
|
64
|
+
frontmatter,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function scanPersonaDirectory(root: string, origin: PersonaOrigin): Promise<DiscoveredPersonaRecord[]> {
|
|
69
|
+
let entries: string[] = [];
|
|
70
|
+
try {
|
|
71
|
+
entries = await fsPromises.readdir(root);
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const records: DiscoveredPersonaRecord[] = [];
|
|
77
|
+
for (const entry of entries.sort((a, b) => a.localeCompare(b))) {
|
|
78
|
+
if (entry.endsWith('.md')) {
|
|
79
|
+
const record = await readPersonaFile(join(root, entry), origin);
|
|
80
|
+
if (record) records.push(record);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const marker of DIRECTORY_MARKERS) {
|
|
85
|
+
const record = await readPersonaFile(join(root, entry, marker), origin);
|
|
86
|
+
if (record) {
|
|
87
|
+
records.push(record);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return records;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function discoverPersonas(shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>): Promise<DiscoveredPersonaRecord[]> {
|
|
97
|
+
const seen = new Set<string>();
|
|
98
|
+
const records: DiscoveredPersonaRecord[] = [];
|
|
99
|
+
|
|
100
|
+
for (const { root, origin } of getPersonaDirectories(shellPaths.workingDirectory, shellPaths.homeDirectory)) {
|
|
101
|
+
for (const record of await scanPersonaDirectory(root, origin)) {
|
|
102
|
+
const key = record.name.toLowerCase();
|
|
103
|
+
if (seen.has(key)) continue;
|
|
104
|
+
seen.add(key);
|
|
105
|
+
records.push(record);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return records.sort((a, b) => {
|
|
110
|
+
const originRank = a.origin === b.origin
|
|
111
|
+
? 0
|
|
112
|
+
: a.origin === 'project-local'
|
|
113
|
+
? -1
|
|
114
|
+
: 1;
|
|
115
|
+
return originRank || a.name.localeCompare(b.name);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { promises as fsPromises } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
|
|
6
|
+
export type RoutineOrigin = 'project-local' | 'global' | 'custom';
|
|
7
|
+
|
|
8
|
+
export interface DiscoveredRoutineRecord {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly origin: RoutineOrigin;
|
|
13
|
+
readonly steps: string;
|
|
14
|
+
readonly frontmatter: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DIRECTORY_MARKERS: readonly string[] = ['ROUTINE.md', 'routine.md'];
|
|
18
|
+
|
|
19
|
+
function parseFrontmatter(content: string): Record<string, string> {
|
|
20
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
+
if (!match) return {};
|
|
22
|
+
const result: Record<string, string> = {};
|
|
23
|
+
for (const line of match[1].split('\n')) {
|
|
24
|
+
const [key, ...rest] = line.split(':');
|
|
25
|
+
if (key && rest.length > 0) {
|
|
26
|
+
result[key.trim()] = rest.join(':').trim();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getRoutineDirectories(cwd: string, homeDir: string): Array<{ root: string; origin: RoutineOrigin }> {
|
|
33
|
+
return [
|
|
34
|
+
{ root: join(cwd, '.goodvibes', 'routines'), origin: 'project-local' },
|
|
35
|
+
{ root: join(cwd, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'routines'), origin: 'project-local' },
|
|
36
|
+
{ root: join(homeDir, '.goodvibes', 'routines'), origin: 'global' },
|
|
37
|
+
{ root: join(homeDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'routines'), origin: 'global' },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readRoutineFile(path: string, origin: RoutineOrigin): Promise<DiscoveredRoutineRecord | null> {
|
|
42
|
+
let content = '';
|
|
43
|
+
try {
|
|
44
|
+
content = await fsPromises.readFile(path, 'utf-8');
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const frontmatter = parseFrontmatter(content);
|
|
50
|
+
const markdownBody = content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
51
|
+
const steps = (frontmatter.steps ?? markdownBody).trim();
|
|
52
|
+
if (!steps) return null;
|
|
53
|
+
const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? 'routine';
|
|
54
|
+
const description = frontmatter.description ?? frontmatter.summary ?? frontmatter.title ?? '';
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name,
|
|
58
|
+
description,
|
|
59
|
+
path,
|
|
60
|
+
origin,
|
|
61
|
+
steps,
|
|
62
|
+
frontmatter,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function scanRoutineDirectory(root: string, origin: RoutineOrigin): Promise<DiscoveredRoutineRecord[]> {
|
|
67
|
+
let entries: string[] = [];
|
|
68
|
+
try {
|
|
69
|
+
entries = await fsPromises.readdir(root);
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const records: DiscoveredRoutineRecord[] = [];
|
|
75
|
+
for (const entry of entries.sort((a, b) => a.localeCompare(b))) {
|
|
76
|
+
if (entry.endsWith('.md')) {
|
|
77
|
+
const record = await readRoutineFile(join(root, entry), origin);
|
|
78
|
+
if (record) records.push(record);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const marker of DIRECTORY_MARKERS) {
|
|
83
|
+
const record = await readRoutineFile(join(root, entry, marker), origin);
|
|
84
|
+
if (record) {
|
|
85
|
+
records.push(record);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return records;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function discoverRoutines(shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>): Promise<DiscoveredRoutineRecord[]> {
|
|
95
|
+
const seen = new Set<string>();
|
|
96
|
+
const records: DiscoveredRoutineRecord[] = [];
|
|
97
|
+
|
|
98
|
+
for (const { root, origin } of getRoutineDirectories(shellPaths.workingDirectory, shellPaths.homeDirectory)) {
|
|
99
|
+
for (const record of await scanRoutineDirectory(root, origin)) {
|
|
100
|
+
const key = record.name.toLowerCase();
|
|
101
|
+
if (seen.has(key)) continue;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
records.push(record);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return records.sort((a, b) => {
|
|
108
|
+
const originRank = a.origin === b.origin
|
|
109
|
+
? 0
|
|
110
|
+
: a.origin === 'project-local'
|
|
111
|
+
? -1
|
|
112
|
+
: 1;
|
|
113
|
+
return originRank || a.name.localeCompare(b.name);
|
|
114
|
+
});
|
|
115
|
+
}
|