@phnx-labs/agents-cli 1.16.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/dist/commands/browser.js +248 -9
- package/dist/commands/cloud.js +8 -0
- package/dist/commands/exec.js +70 -1
- package/dist/commands/plugins.js +179 -5
- package/dist/commands/prune.js +6 -0
- package/dist/commands/secrets.js +117 -19
- package/dist/commands/view.js +21 -8
- package/dist/commands/workflows.d.ts +10 -0
- package/dist/commands/workflows.js +457 -0
- package/dist/index.js +31 -16
- package/dist/lib/browser/cdp.js +7 -4
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +37 -2
- package/dist/lib/browser/drivers/local.js +13 -2
- package/dist/lib/browser/input.d.ts +1 -0
- package/dist/lib/browser/input.js +3 -0
- package/dist/lib/browser/ipc.js +14 -0
- package/dist/lib/browser/profiles.d.ts +5 -0
- package/dist/lib/browser/profiles.js +45 -0
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +29 -1
- package/dist/lib/browser/types.d.ts +11 -1
- package/dist/lib/cloud/rush.d.ts +28 -1
- package/dist/lib/cloud/rush.js +68 -13
- package/dist/lib/commands.d.ts +0 -15
- package/dist/lib/commands.js +5 -5
- package/dist/lib/hooks.js +24 -11
- package/dist/lib/migrate.js +59 -1
- package/dist/lib/permissions.d.ts +0 -58
- package/dist/lib/permissions.js +10 -10
- package/dist/lib/plugins.d.ts +75 -34
- package/dist/lib/plugins.js +640 -133
- package/dist/lib/resource-patterns.d.ts +41 -0
- package/dist/lib/resource-patterns.js +82 -0
- package/dist/lib/resources/index.d.ts +17 -0
- package/dist/lib/resources/index.js +7 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources/workflows.d.ts +24 -0
- package/dist/lib/resources/workflows.js +110 -0
- package/dist/lib/resources.d.ts +6 -1
- package/dist/lib/resources.js +12 -2
- package/dist/lib/session/db.d.ts +18 -0
- package/dist/lib/session/db.js +106 -7
- package/dist/lib/session/discover.d.ts +6 -0
- package/dist/lib/session/discover.js +28 -17
- package/dist/lib/shims.d.ts +3 -51
- package/dist/lib/shims.js +18 -10
- package/dist/lib/sqlite.js +10 -4
- package/dist/lib/state.d.ts +15 -2
- package/dist/lib/state.js +29 -8
- package/dist/lib/types.d.ts +43 -14
- package/dist/lib/versions.d.ts +3 -0
- package/dist/lib/versions.js +139 -27
- package/dist/lib/workflows.d.ts +79 -0
- package/dist/lib/workflows.js +233 -0
- package/package.json +1 -5
- package/scripts/postinstall.js +59 -58
- package/dist/commands/fork.d.ts +0 -10
- package/dist/commands/fork.js +0 -146
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource selection patterns for agents.yaml versions: entries.
|
|
3
|
+
*
|
|
4
|
+
* Pattern syntax: [!]source:name
|
|
5
|
+
* "system:*" — all resources from ~/.agents-system/
|
|
6
|
+
* "user:*" — all resources from ~/.agents/
|
|
7
|
+
* "rush:*" — all resources from ~/.agents-rush/ (extra repo alias)
|
|
8
|
+
* "project:*" — all resources from .agents/ in the project root
|
|
9
|
+
* "user:foo" — specifically the resource named "foo" from ~/.agents/
|
|
10
|
+
* "!user:temp" — exclude "temp" from the user repo
|
|
11
|
+
*
|
|
12
|
+
* Evaluation rule: union all inclusions, then subtract all exclusions.
|
|
13
|
+
*/
|
|
14
|
+
export interface ParsedPattern {
|
|
15
|
+
negate: boolean;
|
|
16
|
+
source: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function parsePattern(p: string): ParsedPattern;
|
|
20
|
+
/** Returns true if the string is a legacy plain name with no source: prefix. */
|
|
21
|
+
export declare function isLegacyName(p: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Expand a list of patterns against an available name→source map.
|
|
24
|
+
* Returns the union of matching names with exclusions subtracted.
|
|
25
|
+
*
|
|
26
|
+
* Supports comma-grouped names to avoid repeating the source prefix:
|
|
27
|
+
* "system:brain-scan,mq" → includes brain-scan and mq from system
|
|
28
|
+
* "!user:temp,draft" → excludes temp and draft from user
|
|
29
|
+
*
|
|
30
|
+
* Note: in YAML flow sequences ([...]) a comma inside a pattern requires
|
|
31
|
+
* quoting ("system:brain-scan,mq"). Block-style items and yaml.stringify
|
|
32
|
+
* output handle this automatically.
|
|
33
|
+
*/
|
|
34
|
+
export declare function expandPatterns(patterns: string[], available: Map<string, string>): string[];
|
|
35
|
+
/**
|
|
36
|
+
* Build the default pattern list for a resource type.
|
|
37
|
+
* Order: system → user → alias1 → alias2 → ... → project (base-to-override).
|
|
38
|
+
* @param extraAliases Alias names of enabled extra repos, in insertion order.
|
|
39
|
+
* @param includeProject Whether to append "project:*". False for hooks (security).
|
|
40
|
+
*/
|
|
41
|
+
export declare function defaultPatterns(extraAliases?: string[], includeProject?: boolean): string[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource selection patterns for agents.yaml versions: entries.
|
|
3
|
+
*
|
|
4
|
+
* Pattern syntax: [!]source:name
|
|
5
|
+
* "system:*" — all resources from ~/.agents-system/
|
|
6
|
+
* "user:*" — all resources from ~/.agents/
|
|
7
|
+
* "rush:*" — all resources from ~/.agents-rush/ (extra repo alias)
|
|
8
|
+
* "project:*" — all resources from .agents/ in the project root
|
|
9
|
+
* "user:foo" — specifically the resource named "foo" from ~/.agents/
|
|
10
|
+
* "!user:temp" — exclude "temp" from the user repo
|
|
11
|
+
*
|
|
12
|
+
* Evaluation rule: union all inclusions, then subtract all exclusions.
|
|
13
|
+
*/
|
|
14
|
+
export function parsePattern(p) {
|
|
15
|
+
const negate = p.startsWith('!');
|
|
16
|
+
const raw = negate ? p.slice(1) : p;
|
|
17
|
+
const colon = raw.indexOf(':');
|
|
18
|
+
if (colon === -1) {
|
|
19
|
+
throw new Error(`Invalid resource pattern "${p}": expected "source:name" format`);
|
|
20
|
+
}
|
|
21
|
+
return { negate, source: raw.slice(0, colon), name: raw.slice(colon + 1) };
|
|
22
|
+
}
|
|
23
|
+
/** Returns true if the string is a legacy plain name with no source: prefix. */
|
|
24
|
+
export function isLegacyName(p) {
|
|
25
|
+
return !p.startsWith('!') && !p.includes(':');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Expand a list of patterns against an available name→source map.
|
|
29
|
+
* Returns the union of matching names with exclusions subtracted.
|
|
30
|
+
*
|
|
31
|
+
* Supports comma-grouped names to avoid repeating the source prefix:
|
|
32
|
+
* "system:brain-scan,mq" → includes brain-scan and mq from system
|
|
33
|
+
* "!user:temp,draft" → excludes temp and draft from user
|
|
34
|
+
*
|
|
35
|
+
* Note: in YAML flow sequences ([...]) a comma inside a pattern requires
|
|
36
|
+
* quoting ("system:brain-scan,mq"). Block-style items and yaml.stringify
|
|
37
|
+
* output handle this automatically.
|
|
38
|
+
*/
|
|
39
|
+
export function expandPatterns(patterns, available) {
|
|
40
|
+
const included = new Set();
|
|
41
|
+
const excluded = new Set();
|
|
42
|
+
for (const p of patterns) {
|
|
43
|
+
try {
|
|
44
|
+
const { negate, source, name } = parsePattern(p);
|
|
45
|
+
const target = negate ? excluded : included;
|
|
46
|
+
// Comma-grouped names: "system:brain-scan,mq" → ['brain-scan', 'mq']
|
|
47
|
+
const names = name === '*' ? ['*'] : name.split(',').map(n => n.trim()).filter(Boolean);
|
|
48
|
+
for (const n of names) {
|
|
49
|
+
if (n === '*') {
|
|
50
|
+
for (const [rn, rs] of available) {
|
|
51
|
+
if (rs === source)
|
|
52
|
+
target.add(rn);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
if (available.has(n))
|
|
57
|
+
target.add(n);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Skip malformed patterns
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return [...included].filter(n => !excluded.has(n));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build the default pattern list for a resource type.
|
|
69
|
+
* Order: system → user → alias1 → alias2 → ... → project (base-to-override).
|
|
70
|
+
* @param extraAliases Alias names of enabled extra repos, in insertion order.
|
|
71
|
+
* @param includeProject Whether to append "project:*". False for hooks (security).
|
|
72
|
+
*/
|
|
73
|
+
export function defaultPatterns(extraAliases = [], includeProject = true) {
|
|
74
|
+
const patterns = ['system:*', 'user:*'];
|
|
75
|
+
for (const alias of extraAliases) {
|
|
76
|
+
patterns.push(`${alias}:*`);
|
|
77
|
+
}
|
|
78
|
+
if (includeProject) {
|
|
79
|
+
patterns.push('project:*');
|
|
80
|
+
}
|
|
81
|
+
return patterns;
|
|
82
|
+
}
|
|
@@ -13,6 +13,7 @@ export { RulesHandler, type RuleItem } from './rules.js';
|
|
|
13
13
|
export { McpHandler, getMcpConfigPath, type McpItem } from './mcp.js';
|
|
14
14
|
export { PermissionsHandler, type PermissionItem } from './permissions.js';
|
|
15
15
|
export { SubagentsHandler, subagentsHandler, type SubagentItem } from './subagents.js';
|
|
16
|
+
export { WorkflowsHandler, type WorkflowItem } from './workflows.js';
|
|
16
17
|
import type { ResourceKind, ResourceHandler } from './types.js';
|
|
17
18
|
/** All resource handlers keyed by kind. */
|
|
18
19
|
export declare const handlers: {
|
|
@@ -29,6 +30,22 @@ export declare const handlers: {
|
|
|
29
30
|
readonly permissions: ResourceHandler<import("../types.js").PermissionSet>;
|
|
30
31
|
readonly subagent: import("./subagents.js").SubagentsHandler;
|
|
31
32
|
readonly subagents: import("./subagents.js").SubagentsHandler;
|
|
33
|
+
readonly workflow: {
|
|
34
|
+
readonly kind: "workflow";
|
|
35
|
+
listAll(_agent: import("./types.js").AgentId, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem>[];
|
|
36
|
+
resolve(_agent: import("./types.js").AgentId, name: string, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem> | null;
|
|
37
|
+
sync(_agent: import("./types.js").AgentId, _versionHome: string, _cwd?: string): void;
|
|
38
|
+
format(_agent: import("./types.js").AgentId): "md";
|
|
39
|
+
targetDir(_agent: import("./types.js").AgentId): string;
|
|
40
|
+
};
|
|
41
|
+
readonly workflows: {
|
|
42
|
+
readonly kind: "workflow";
|
|
43
|
+
listAll(_agent: import("./types.js").AgentId, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem>[];
|
|
44
|
+
resolve(_agent: import("./types.js").AgentId, name: string, cwd?: string): import("./types.js").ResolvedItem<import("./workflows.js").WorkflowItem> | null;
|
|
45
|
+
sync(_agent: import("./types.js").AgentId, _versionHome: string, _cwd?: string): void;
|
|
46
|
+
format(_agent: import("./types.js").AgentId): "md";
|
|
47
|
+
targetDir(_agent: import("./types.js").AgentId): string;
|
|
48
|
+
};
|
|
32
49
|
};
|
|
33
50
|
/** Get a handler by resource kind. */
|
|
34
51
|
export declare function getHandler(kind: ResourceKind): ResourceHandler<unknown> | null;
|
|
@@ -13,6 +13,7 @@ export { RulesHandler } from './rules.js';
|
|
|
13
13
|
export { McpHandler, getMcpConfigPath } from './mcp.js';
|
|
14
14
|
export { PermissionsHandler } from './permissions.js';
|
|
15
15
|
export { SubagentsHandler, subagentsHandler } from './subagents.js';
|
|
16
|
+
export { WorkflowsHandler } from './workflows.js';
|
|
16
17
|
import { commandsHandler } from './commands.js';
|
|
17
18
|
import { HooksHandler } from './hooks.js';
|
|
18
19
|
import { SkillsHandler } from './skills.js';
|
|
@@ -20,6 +21,7 @@ import { RulesHandler } from './rules.js';
|
|
|
20
21
|
import { McpHandler } from './mcp.js';
|
|
21
22
|
import { PermissionsHandler } from './permissions.js';
|
|
22
23
|
import { subagentsHandler } from './subagents.js';
|
|
24
|
+
import { WorkflowsHandler } from './workflows.js';
|
|
23
25
|
/** All resource handlers keyed by kind. */
|
|
24
26
|
export const handlers = {
|
|
25
27
|
command: commandsHandler,
|
|
@@ -35,6 +37,8 @@ export const handlers = {
|
|
|
35
37
|
permissions: PermissionsHandler,
|
|
36
38
|
subagent: subagentsHandler,
|
|
37
39
|
subagents: subagentsHandler,
|
|
40
|
+
workflow: WorkflowsHandler,
|
|
41
|
+
workflows: WorkflowsHandler,
|
|
38
42
|
};
|
|
39
43
|
/** Get a handler by resource kind. */
|
|
40
44
|
export function getHandler(kind) {
|
|
@@ -53,6 +57,8 @@ export function getHandler(kind) {
|
|
|
53
57
|
return PermissionsHandler;
|
|
54
58
|
case 'subagent':
|
|
55
59
|
return subagentsHandler;
|
|
60
|
+
case 'workflow':
|
|
61
|
+
return WorkflowsHandler;
|
|
56
62
|
default:
|
|
57
63
|
return null;
|
|
58
64
|
}
|
|
@@ -66,4 +72,5 @@ export const RESOURCE_KINDS = [
|
|
|
66
72
|
'mcp',
|
|
67
73
|
'permission',
|
|
68
74
|
'subagent',
|
|
75
|
+
'workflow',
|
|
69
76
|
];
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw';
|
|
9
9
|
export type Layer = 'system' | 'user' | 'project';
|
|
10
|
-
export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent';
|
|
10
|
+
export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow';
|
|
11
11
|
/** A resolved resource with its origin layer. */
|
|
12
12
|
export interface ResolvedItem<T> {
|
|
13
13
|
name: string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflows resource handler.
|
|
3
|
+
*
|
|
4
|
+
* Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
|
|
5
|
+
* They optionally contain subagents/, skills/, and plugins/ subdirectories.
|
|
6
|
+
* Resolution order: project > user > system.
|
|
7
|
+
*/
|
|
8
|
+
import type { AgentId, ResolvedItem, ResourceHandler } from './types.js';
|
|
9
|
+
export interface WorkflowItem {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
model?: string;
|
|
13
|
+
subagentCount: number;
|
|
14
|
+
}
|
|
15
|
+
declare class WorkflowsHandlerImpl implements ResourceHandler<WorkflowItem> {
|
|
16
|
+
readonly kind: "workflow";
|
|
17
|
+
listAll(_agent: AgentId, cwd?: string): ResolvedItem<WorkflowItem>[];
|
|
18
|
+
resolve(_agent: AgentId, name: string, cwd?: string): ResolvedItem<WorkflowItem> | null;
|
|
19
|
+
sync(_agent: AgentId, _versionHome: string, _cwd?: string): void;
|
|
20
|
+
format(_agent: AgentId): 'md';
|
|
21
|
+
targetDir(_agent: AgentId): string;
|
|
22
|
+
}
|
|
23
|
+
export declare const WorkflowsHandler: WorkflowsHandlerImpl;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflows resource handler.
|
|
3
|
+
*
|
|
4
|
+
* Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
|
|
5
|
+
* They optionally contain subagents/, skills/, and plugins/ subdirectories.
|
|
6
|
+
* Resolution order: project > user > system.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { getProjectAgentsDir, getUserWorkflowsDir, getSystemWorkflowsDir, getEnabledExtraRepos, } from '../state.js';
|
|
11
|
+
import { parseWorkflowFrontmatter, countWorkflowSubagents } from '../workflows.js';
|
|
12
|
+
function getLayerDirs(cwd) {
|
|
13
|
+
const projectDir = getProjectAgentsDir(cwd);
|
|
14
|
+
const extraRepos = getEnabledExtraRepos();
|
|
15
|
+
return {
|
|
16
|
+
system: getSystemWorkflowsDir(),
|
|
17
|
+
user: getUserWorkflowsDir(),
|
|
18
|
+
project: projectDir ? path.join(projectDir, 'workflows') : null,
|
|
19
|
+
extra: extraRepos.map(e => path.join(e.dir, 'workflows')),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function listWorkflowsInDir(dir) {
|
|
23
|
+
if (!fs.existsSync(dir))
|
|
24
|
+
return [];
|
|
25
|
+
try {
|
|
26
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
27
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.') &&
|
|
28
|
+
fs.existsSync(path.join(dir, e.name, 'WORKFLOW.md')))
|
|
29
|
+
.map(e => ({ name: e.name, path: path.join(dir, e.name) }));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
class WorkflowsHandlerImpl {
|
|
36
|
+
kind = 'workflow';
|
|
37
|
+
listAll(_agent, cwd) {
|
|
38
|
+
const dirs = getLayerDirs(cwd);
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const results = [];
|
|
41
|
+
const layerDirs = [];
|
|
42
|
+
if (dirs.project)
|
|
43
|
+
layerDirs.push({ dir: dirs.project, layer: 'project' });
|
|
44
|
+
layerDirs.push({ dir: dirs.user, layer: 'user' });
|
|
45
|
+
layerDirs.push({ dir: dirs.system, layer: 'system' });
|
|
46
|
+
for (const extraDir of dirs.extra)
|
|
47
|
+
layerDirs.push({ dir: extraDir, layer: 'system' });
|
|
48
|
+
for (const { dir, layer } of layerDirs) {
|
|
49
|
+
for (const { name, path: workflowPath } of listWorkflowsInDir(dir)) {
|
|
50
|
+
if (seen.has(name))
|
|
51
|
+
continue;
|
|
52
|
+
const fm = parseWorkflowFrontmatter(workflowPath);
|
|
53
|
+
if (!fm)
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(name);
|
|
56
|
+
results.push({
|
|
57
|
+
name,
|
|
58
|
+
item: {
|
|
59
|
+
name: fm.name || name,
|
|
60
|
+
description: fm.description,
|
|
61
|
+
model: fm.model,
|
|
62
|
+
subagentCount: countWorkflowSubagents(workflowPath),
|
|
63
|
+
},
|
|
64
|
+
layer,
|
|
65
|
+
path: workflowPath,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
70
|
+
}
|
|
71
|
+
resolve(_agent, name, cwd) {
|
|
72
|
+
const dirs = getLayerDirs(cwd);
|
|
73
|
+
const searchDirs = [];
|
|
74
|
+
if (dirs.project)
|
|
75
|
+
searchDirs.push({ dir: dirs.project, layer: 'project' });
|
|
76
|
+
searchDirs.push({ dir: dirs.user, layer: 'user' });
|
|
77
|
+
searchDirs.push({ dir: dirs.system, layer: 'system' });
|
|
78
|
+
for (const extraDir of dirs.extra)
|
|
79
|
+
searchDirs.push({ dir: extraDir, layer: 'system' });
|
|
80
|
+
for (const { dir, layer } of searchDirs) {
|
|
81
|
+
const workflowPath = path.join(dir, name);
|
|
82
|
+
const fm = parseWorkflowFrontmatter(workflowPath);
|
|
83
|
+
if (fm) {
|
|
84
|
+
return {
|
|
85
|
+
name,
|
|
86
|
+
item: {
|
|
87
|
+
name: fm.name || name,
|
|
88
|
+
description: fm.description,
|
|
89
|
+
model: fm.model,
|
|
90
|
+
subagentCount: countWorkflowSubagents(workflowPath),
|
|
91
|
+
},
|
|
92
|
+
layer,
|
|
93
|
+
path: workflowPath,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
sync(_agent, _versionHome, _cwd) {
|
|
100
|
+
// Version-home copies are written by syncResourcesToVersion in versions.ts.
|
|
101
|
+
// exec.ts resolves workflows at run time from source dirs directly.
|
|
102
|
+
}
|
|
103
|
+
format(_agent) {
|
|
104
|
+
return 'md';
|
|
105
|
+
}
|
|
106
|
+
targetDir(_agent) {
|
|
107
|
+
return 'workflows';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export const WorkflowsHandler = new WorkflowsHandlerImpl();
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ export interface ResolvedResource {
|
|
|
11
11
|
name: string;
|
|
12
12
|
/** Absolute path to the resource file or directory. */
|
|
13
13
|
path: string;
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Source layer: 'project' | 'user' | 'system' for built-in layers,
|
|
16
|
+
* or the alias name (e.g. 'rush') for extra repos registered in agents.yaml.
|
|
17
|
+
*/
|
|
18
|
+
source: string;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
21
|
* Resolve a single resource by kind + name using project > user > system precedence.
|
|
@@ -52,6 +56,7 @@ export interface AgentResources {
|
|
|
52
56
|
mcp: McpResourceEntry[];
|
|
53
57
|
memory: ResourceEntry[];
|
|
54
58
|
hooks: ResourceEntry[];
|
|
59
|
+
workflows: ResourceEntry[];
|
|
55
60
|
}
|
|
56
61
|
/** Options for resource discovery. */
|
|
57
62
|
export interface GetAgentResourcesOptions {
|
package/dist/lib/resources.js
CHANGED
|
@@ -11,6 +11,8 @@ import { listInstalledHooksWithScope } from './hooks.js';
|
|
|
11
11
|
import { listInstalledInstructionsWithScope } from './rules/rules.js';
|
|
12
12
|
import { getEffectiveHome } from './versions.js';
|
|
13
13
|
import { listMcpServerConfigs } from './mcp.js';
|
|
14
|
+
import { WorkflowsHandler } from './resources/workflows.js';
|
|
15
|
+
import { WORKFLOW_CAPABLE_AGENTS } from './workflows.js';
|
|
14
16
|
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, } from './state.js';
|
|
15
17
|
/**
|
|
16
18
|
* Resolve a single resource by kind + name using project > user > system precedence.
|
|
@@ -26,7 +28,7 @@ export function resolveResource(kind, name, cwd) {
|
|
|
26
28
|
...(projectDir ? [[path.join(projectDir, kind), 'project']] : []),
|
|
27
29
|
[path.join(getUserAgentsDir(), kind), 'user'],
|
|
28
30
|
[path.join(getSystemAgentsDir(), kind), 'system'],
|
|
29
|
-
...extraRepos.map((e) => [path.join(e.dir, kind),
|
|
31
|
+
...extraRepos.map((e) => [path.join(e.dir, kind), e.alias]),
|
|
30
32
|
];
|
|
31
33
|
for (const [dir, source] of candidates) {
|
|
32
34
|
if (!fs.existsSync(dir))
|
|
@@ -60,7 +62,7 @@ export function listResources(kind, cwd) {
|
|
|
60
62
|
...(projectDir ? [[path.join(projectDir, kind), 'project']] : []),
|
|
61
63
|
[path.join(getUserAgentsDir(), kind), 'user'],
|
|
62
64
|
[path.join(getSystemAgentsDir(), kind), 'system'],
|
|
63
|
-
...extraRepos.map((e) => [path.join(e.dir, kind),
|
|
65
|
+
...extraRepos.map((e) => [path.join(e.dir, kind), e.alias]),
|
|
64
66
|
];
|
|
65
67
|
for (const [dir, source] of roots) {
|
|
66
68
|
if (!fs.existsSync(dir))
|
|
@@ -159,6 +161,13 @@ export function getAgentResources(agentId, options = {}) {
|
|
|
159
161
|
hooks.push({ name: hook.name, path: hook.path, scope: hook.scope });
|
|
160
162
|
}
|
|
161
163
|
}
|
|
164
|
+
// Workflows (claude only)
|
|
165
|
+
const workflows = [];
|
|
166
|
+
if (WORKFLOW_CAPABLE_AGENTS.includes(agentId)) {
|
|
167
|
+
for (const w of WorkflowsHandler.listAll(agentId, cwd)) {
|
|
168
|
+
workflows.push({ name: w.name, path: w.path, scope: w.layer === 'project' ? 'project' : 'user' });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
162
171
|
return {
|
|
163
172
|
agentId,
|
|
164
173
|
commands,
|
|
@@ -167,6 +176,7 @@ export function getAgentResources(agentId, options = {}) {
|
|
|
167
176
|
mcp,
|
|
168
177
|
memory,
|
|
169
178
|
hooks,
|
|
179
|
+
workflows,
|
|
170
180
|
};
|
|
171
181
|
}
|
|
172
182
|
/**
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -55,6 +55,24 @@ export interface QueryOptions {
|
|
|
55
55
|
export declare function getDB(): Database.Database;
|
|
56
56
|
/** Close the cached database connection. */
|
|
57
57
|
export declare function closeDB(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Try to claim the right to run the incremental scan. Returns true if this
|
|
60
|
+
* process should proceed with scanning, false if another live process is
|
|
61
|
+
* already scanning (caller should skip the scan and serve from the DB).
|
|
62
|
+
*
|
|
63
|
+
* Uses the `meta` table so it survives crashes — dead PIDs are detected via
|
|
64
|
+
* process.kill(pid, 0), stale entries via TTL. No external lock files needed.
|
|
65
|
+
*
|
|
66
|
+
* Wrapped in db.transaction() (BEGIN IMMEDIATE) so the read-then-write is
|
|
67
|
+
* atomic and busy_timeout retries correctly — bare auto-commit DML in WAL
|
|
68
|
+
* mode can return SQLITE_BUSY_SNAPSHOT which bypasses the busy handler.
|
|
69
|
+
*/
|
|
70
|
+
export declare function tryClaimScan(pid: number): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Release the scan claim written by tryClaimScan. Only deletes the entry
|
|
73
|
+
* if it still belongs to this process (guards against TTL takeovers).
|
|
74
|
+
*/
|
|
75
|
+
export declare function releaseScan(pid: number): void;
|
|
58
76
|
/** Return the absolute path to the sessions database file. */
|
|
59
77
|
export declare function getDBPath(): string;
|
|
60
78
|
/**
|
package/dist/lib/session/db.js
CHANGED
|
@@ -151,16 +151,17 @@ export function getDB() {
|
|
|
151
151
|
db.pragma('journal_mode = WAL');
|
|
152
152
|
db.pragma('synchronous = NORMAL');
|
|
153
153
|
db.pragma('temp_store = MEMORY');
|
|
154
|
-
// Wait up to
|
|
155
|
-
// agents (CLIs,
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
154
|
+
// Wait up to 30s instead of failing immediately on SQLITE_BUSY. Multiple
|
|
155
|
+
// agents (CLIs, skills, hooks) open this DB concurrently. The first scan of
|
|
156
|
+
// a new version home can take longer than 10s; concurrent callers need enough
|
|
157
|
+
// headroom to wait. The ledger-recheck in upsertSessionsBatch makes
|
|
158
|
+
// subsequent writers near-instant, so 30s is a rarely-reached safety net.
|
|
159
|
+
db.pragma('busy_timeout = 30000');
|
|
159
160
|
db.exec(SCHEMA);
|
|
160
161
|
const current = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
161
162
|
const currentVersion = current ? parseInt(current.value, 10) : 0;
|
|
162
163
|
if (!current) {
|
|
163
|
-
db.prepare(`INSERT INTO meta(key, value) VALUES ('schema_version', ?)`).run(String(SCHEMA_VERSION));
|
|
164
|
+
db.prepare(`INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', ?)`).run(String(SCHEMA_VERSION));
|
|
164
165
|
}
|
|
165
166
|
else if (currentVersion < SCHEMA_VERSION) {
|
|
166
167
|
migrateSchema(db, currentVersion);
|
|
@@ -181,7 +182,7 @@ export function getDB() {
|
|
|
181
182
|
}
|
|
182
183
|
catch { /* ignore */ }
|
|
183
184
|
}
|
|
184
|
-
db.prepare(`INSERT INTO meta(key, value) VALUES ('legacy_indexes_removed', '1')`).run();
|
|
185
|
+
db.prepare(`INSERT OR IGNORE INTO meta(key, value) VALUES ('legacy_indexes_removed', '1')`).run();
|
|
185
186
|
}
|
|
186
187
|
dbInstance = db;
|
|
187
188
|
return db;
|
|
@@ -193,6 +194,75 @@ export function closeDB() {
|
|
|
193
194
|
dbInstance = null;
|
|
194
195
|
}
|
|
195
196
|
}
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Scan coordinator — prevents concurrent full scans across processes
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
/** How long a scan claim is trusted before it's considered stale (ms). */
|
|
201
|
+
const SCAN_CLAIM_TTL_MS = 120_000; // 2 minutes
|
|
202
|
+
function isProcessAlive(pid) {
|
|
203
|
+
if (!pid || isNaN(pid))
|
|
204
|
+
return false;
|
|
205
|
+
try {
|
|
206
|
+
process.kill(pid, 0);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Try to claim the right to run the incremental scan. Returns true if this
|
|
215
|
+
* process should proceed with scanning, false if another live process is
|
|
216
|
+
* already scanning (caller should skip the scan and serve from the DB).
|
|
217
|
+
*
|
|
218
|
+
* Uses the `meta` table so it survives crashes — dead PIDs are detected via
|
|
219
|
+
* process.kill(pid, 0), stale entries via TTL. No external lock files needed.
|
|
220
|
+
*
|
|
221
|
+
* Wrapped in db.transaction() (BEGIN IMMEDIATE) so the read-then-write is
|
|
222
|
+
* atomic and busy_timeout retries correctly — bare auto-commit DML in WAL
|
|
223
|
+
* mode can return SQLITE_BUSY_SNAPSHOT which bypasses the busy handler.
|
|
224
|
+
*/
|
|
225
|
+
export function tryClaimScan(pid) {
|
|
226
|
+
const db = getDB();
|
|
227
|
+
const txn = db.transaction(() => {
|
|
228
|
+
const existing = db
|
|
229
|
+
.prepare(`SELECT value FROM meta WHERE key = 'scan_in_progress'`)
|
|
230
|
+
.get();
|
|
231
|
+
if (existing) {
|
|
232
|
+
const parts = existing.value.split(':');
|
|
233
|
+
const existingPid = parseInt(parts[0], 10);
|
|
234
|
+
const existingTs = parseInt(parts[1], 10);
|
|
235
|
+
const ageMs = Date.now() - existingTs;
|
|
236
|
+
if (isProcessAlive(existingPid) && ageMs < SCAN_CLAIM_TTL_MS) {
|
|
237
|
+
return false; // another live process is scanning — skip
|
|
238
|
+
}
|
|
239
|
+
// Dead PID or expired TTL — take over below
|
|
240
|
+
}
|
|
241
|
+
db.prepare(`INSERT OR REPLACE INTO meta (key, value) VALUES ('scan_in_progress', ?)`)
|
|
242
|
+
.run(`${pid}:${Date.now()}`);
|
|
243
|
+
return true;
|
|
244
|
+
});
|
|
245
|
+
return txn();
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Release the scan claim written by tryClaimScan. Only deletes the entry
|
|
249
|
+
* if it still belongs to this process (guards against TTL takeovers).
|
|
250
|
+
*/
|
|
251
|
+
export function releaseScan(pid) {
|
|
252
|
+
const db = getDB();
|
|
253
|
+
const txn = db.transaction(() => {
|
|
254
|
+
const existing = db
|
|
255
|
+
.prepare(`SELECT value FROM meta WHERE key = 'scan_in_progress'`)
|
|
256
|
+
.get();
|
|
257
|
+
if (!existing)
|
|
258
|
+
return;
|
|
259
|
+
const claimPid = parseInt(existing.value.split(':')[0], 10);
|
|
260
|
+
if (claimPid === pid) {
|
|
261
|
+
db.prepare(`DELETE FROM meta WHERE key = 'scan_in_progress'`).run();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
txn();
|
|
265
|
+
}
|
|
196
266
|
/** Return the absolute path to the sessions database file. */
|
|
197
267
|
export function getDBPath() {
|
|
198
268
|
return DB_PATH;
|
|
@@ -367,8 +437,37 @@ export function upsertSessionsBatch(entries) {
|
|
|
367
437
|
file_size = excluded.file_size,
|
|
368
438
|
scanned_at = excluded.scanned_at
|
|
369
439
|
`);
|
|
440
|
+
// Build a lookup from canonical file path → entry, used inside the write
|
|
441
|
+
// transaction to re-check the ledger AFTER acquiring the lock. When a
|
|
442
|
+
// concurrent process already committed the same files between our
|
|
443
|
+
// filterChangedFiles call and now, the ledger will have matching (mtime, size)
|
|
444
|
+
// rows — we skip those entries, making the second writer's transaction a
|
|
445
|
+
// near-instant no-op rather than redundant work.
|
|
446
|
+
const byPath = new Map(entries
|
|
447
|
+
.filter(e => e.scan && e.meta.filePath)
|
|
448
|
+
.map(e => [canonicalLedgerKey(e.meta.filePath), e]));
|
|
370
449
|
const txn = db.transaction((items) => {
|
|
450
|
+
// Re-read the ledger now that we hold the write lock. Any file committed
|
|
451
|
+
// by a concurrent process since our pre-scan is visible here.
|
|
452
|
+
const CHUNK = 500; // stay under SQLite's 999-variable limit
|
|
453
|
+
const alreadyIndexed = new Set();
|
|
454
|
+
const paths = [...byPath.keys()];
|
|
455
|
+
for (let i = 0; i < paths.length; i += CHUNK) {
|
|
456
|
+
const chunk = paths.slice(i, i + CHUNK);
|
|
457
|
+
const phs = chunk.map(() => '?').join(',');
|
|
458
|
+
const rows = db
|
|
459
|
+
.prepare(`SELECT file_path, file_mtime_ms, file_size FROM scan_ledger WHERE file_path IN (${phs})`)
|
|
460
|
+
.all(...chunk);
|
|
461
|
+
for (const row of rows) {
|
|
462
|
+
const entry = byPath.get(row.file_path);
|
|
463
|
+
if (entry && row.file_mtime_ms === entry.scan.fileMtimeMs && row.file_size === entry.scan.fileSize) {
|
|
464
|
+
alreadyIndexed.add(entry.meta.id);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
371
468
|
for (const { meta, content, scan } of items) {
|
|
469
|
+
if (alreadyIndexed.has(meta.id))
|
|
470
|
+
continue;
|
|
372
471
|
upsert.run({
|
|
373
472
|
id: meta.id,
|
|
374
473
|
short_id: meta.shortId,
|
|
@@ -37,6 +37,12 @@ export interface ScanProgress {
|
|
|
37
37
|
/**
|
|
38
38
|
* Discover sessions. Scans only files whose (mtime, size) have changed since
|
|
39
39
|
* the last run; everything else is served from the SQLite cache.
|
|
40
|
+
*
|
|
41
|
+
* Only one process runs the incremental scan at a time. When many agents boot
|
|
42
|
+
* simultaneously (e.g. after a restart), the first to claim the scan slot does
|
|
43
|
+
* the work; the rest skip parsing entirely and serve from the DB. The claim is
|
|
44
|
+
* stored in the `meta` table — crash-safe via dead-PID detection and a 2-min
|
|
45
|
+
* TTL, no external lock files needed.
|
|
40
46
|
*/
|
|
41
47
|
export declare function discoverSessions(options?: DiscoverOptions): Promise<SessionMeta[]>;
|
|
42
48
|
/**
|