@vellumai/assistant 0.10.9-staging.1 → 0.10.9-staging.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/AGENTS.md +4 -0
- package/knip.json +1 -4
- package/openapi.yaml +28 -18
- package/package.json +1 -1
- package/src/__tests__/always-loaded-tools-guard.test.ts +4 -6
- package/src/__tests__/app-routes-csp.test.ts +13 -4
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +4 -8
- package/src/__tests__/disk-pressure-tools.test.ts +4 -4
- package/src/__tests__/file-list-tool.test.ts +22 -12
- package/src/__tests__/list-all-apps.test.ts +204 -0
- package/src/__tests__/messaging-send-tool.test.ts +67 -0
- package/src/__tests__/mtime-cache.test.ts +42 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +3 -2
- package/src/__tests__/outlook-messaging-provider.test.ts +71 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +163 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +47 -0
- package/src/__tests__/subagent-notify-parent.test.ts +4 -3
- package/src/__tests__/subagent-tool-filtering.test.ts +21 -21
- package/src/__tests__/subagent-tool-gate-mode.test.ts +6 -6
- package/src/apps/app-store.ts +302 -2
- package/src/cli/AGENTS.md +17 -0
- package/src/cli/commands/__tests__/gateway.test.ts +55 -0
- package/src/cli/commands/apps.help.ts +17 -11
- package/src/cli/commands/apps.ts +5 -15
- package/src/cli/commands/gateway.help.ts +30 -0
- package/src/cli/commands/gateway.ts +40 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -90
- package/src/cli/commands/platform/index.help.ts +3 -2
- package/src/cli/commands/platform/index.ts +0 -9
- package/src/cli/commands/routes.help.ts +13 -7
- package/src/cli/commands/routes.ts +5 -3
- package/src/cli/commands/status.help.ts +24 -0
- package/src/cli/commands/status.ts +29 -0
- package/src/cli/lib/__tests__/global-json-option.test.ts +97 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +1 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +31 -0
- package/src/cli/lib/bundled-marketplace.json +187 -173
- package/src/cli/lib/global-json-option.ts +41 -0
- package/src/cli/lib/plugin-fingerprint.ts +18 -7
- package/src/cli/program.ts +5 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +27 -19
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/feature-flag-registry.json +1 -9
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +3 -6
- package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +3 -6
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +8 -5
- package/src/daemon/conversation-tool-setup.ts +5 -90
- package/src/daemon/conversation.ts +2 -2
- package/src/messaging/provider-types.ts +9 -0
- package/src/messaging/providers/outlook/adapter.ts +17 -2
- package/src/messaging/providers/outlook/client.ts +11 -2
- package/src/messaging/providers/outlook/types.ts +9 -0
- package/src/monitoring/__tests__/plugin-source-watch.test.ts +37 -0
- package/src/monitoring/plugin-source-watch.ts +53 -0
- package/src/oauth/seed-providers.ts +6 -0
- package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +3 -0
- package/src/persistence/embeddings/messages-lexical-index.ts +11 -0
- package/src/plugin-api/conversation-turn.ts +234 -0
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/__tests__/source-fingerprint.test.ts +41 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +48 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +46 -5
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +2 -2
- package/src/plugins/defaults/memory/graph-topology/__tests__/build-memory-graph.test.ts +81 -2
- package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +80 -33
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +40 -1
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +28 -2
- package/src/plugins/mtime-cache.ts +26 -13
- package/src/plugins/plugin-tree-walk.ts +30 -0
- package/src/plugins/source-fingerprint.ts +9 -2
- package/src/plugins/surface-import.ts +28 -0
- package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +1 -1
- package/src/providers/speech-to-text/__tests__/vellum-speech-relay-connection.test.ts +4 -1
- package/src/providers/speech-to-text/vellum-speech-relay-connection.ts +5 -0
- package/src/runtime/routes/__tests__/gateway-status-routes.test.ts +84 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +47 -3
- package/src/runtime/routes/__tests__/user-routes-cli.test.ts +154 -0
- package/src/runtime/routes/app-management-routes.ts +96 -32
- package/src/runtime/routes/app-routes.ts +33 -17
- package/src/runtime/routes/gateway-status-routes.ts +69 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +4 -14
- package/src/runtime/routes/plugins-routes.ts +58 -1
- package/src/runtime/routes/settings-routes.ts +8 -6
- package/src/runtime/routes/user-route-dispatcher.ts +6 -86
- package/src/runtime/routes/user-route-resolution.ts +141 -0
- package/src/runtime/routes/user-routes-cli.ts +56 -35
- package/src/telemetry/AGENTS.md +15 -0
- package/src/telemetry/__tests__/telemetry-event-fixtures.ts +258 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire-validation.test.ts +157 -0
- package/src/telemetry/telemetry-wire-validation.ts +125 -0
- package/src/telemetry/telemetry-wire.generated.ts +60 -9
- package/src/telemetry/types.test.ts +38 -0
- package/src/telemetry/types.ts +183 -91
- package/src/telemetry/usage-telemetry-reporter.ts +5 -0
- package/src/telemetry/watchdog-direct-emit.ts +6 -0
- package/src/tools/skills/scaffold-managed.ts +43 -11
package/src/apps/app-store.ts
CHANGED
|
@@ -35,10 +35,11 @@ import {
|
|
|
35
35
|
} from "node:path";
|
|
36
36
|
|
|
37
37
|
import { rawAll } from "../persistence/raw-query.js";
|
|
38
|
+
import { isPluginDisabled } from "../plugins/disabled-state.js";
|
|
38
39
|
import type { EditEngineResult } from "../tools/shared/filesystem/edit-engine.js";
|
|
39
40
|
import { applyEdit } from "../tools/shared/filesystem/edit-engine.js";
|
|
40
41
|
import { getLogger } from "../util/logger.js";
|
|
41
|
-
import { getDataDir } from "../util/platform.js";
|
|
42
|
+
import { getDataDir, getWorkspacePluginsDir } from "../util/platform.js";
|
|
42
43
|
|
|
43
44
|
export interface AppDefinition {
|
|
44
45
|
id: string;
|
|
@@ -85,6 +86,28 @@ export function resolveEffectiveAppHtml(app: AppDefinition): string {
|
|
|
85
86
|
return `<p>App compilation failed. Edit a source file to trigger a rebuild.</p>`;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the effective, self-contained HTML for an app given its source
|
|
91
|
+
* directory and format, without needing a loaded {@link AppDefinition}. Used
|
|
92
|
+
* to render apps addressed by path (e.g. plugin-bundled apps). Single-file
|
|
93
|
+
* apps serve their root index.html; multi-file apps serve the compiled
|
|
94
|
+
* dist/index.html with JS/CSS inlined.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveEffectiveAppHtmlFromDir(
|
|
97
|
+
sourceDir: string,
|
|
98
|
+
formatVersion: number,
|
|
99
|
+
): string {
|
|
100
|
+
if (formatVersion !== 2) {
|
|
101
|
+
const indexPath = join(sourceDir, "index.html");
|
|
102
|
+
return existsSync(indexPath) ? readFileSync(indexPath, "utf-8") : "";
|
|
103
|
+
}
|
|
104
|
+
const distIndex = join(sourceDir, "dist", "index.html");
|
|
105
|
+
if (existsSync(distIndex)) {
|
|
106
|
+
return inlineDistAssets(sourceDir, readFileSync(distIndex, "utf-8"));
|
|
107
|
+
}
|
|
108
|
+
return `<p>App compilation failed. Edit a source file to trigger a rebuild.</p>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
/**
|
|
89
112
|
* Inline dist assets (main.js, main.css) into the compiled HTML so it can be
|
|
90
113
|
* delivered as a self-contained string via loadHTMLString/SSE without needing
|
|
@@ -377,6 +400,16 @@ function invalidateDirNameCache(appId?: string): void {
|
|
|
377
400
|
* Returns the resolved absolute path.
|
|
378
401
|
*/
|
|
379
402
|
function validateFilePath(appId: string, path: string): string {
|
|
403
|
+
return validateFilePathInDir(getAppDirPath(appId), path);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Validate a relative file path within a known app directory (absolute path).
|
|
408
|
+
* Prevents path traversal and access to protected directories. Used when the
|
|
409
|
+
* app directory is already resolved (e.g. plugin-bundled apps addressed by
|
|
410
|
+
* path rather than by a workspace app id).
|
|
411
|
+
*/
|
|
412
|
+
function validateFilePathInDir(appDir: string, path: string): string {
|
|
380
413
|
if (!path || path.trim() === "") {
|
|
381
414
|
throw new Error(`Invalid file path: path is empty`);
|
|
382
415
|
}
|
|
@@ -391,7 +424,6 @@ function validateFilePath(appId: string, path: string): string {
|
|
|
391
424
|
if (normalized === "records" || normalized.startsWith("records/")) {
|
|
392
425
|
throw new Error(`Invalid file path: 'records/' directory is protected`);
|
|
393
426
|
}
|
|
394
|
-
const appDir = getAppDirPath(appId);
|
|
395
427
|
const resolved = resolve(appDir, path);
|
|
396
428
|
// Ensure the resolved path is still within the app directory
|
|
397
429
|
if (!resolved.startsWith(appDir + "/") && resolved !== appDir) {
|
|
@@ -624,6 +656,257 @@ export function listApps(): AppDefinition[] {
|
|
|
624
656
|
return apps;
|
|
625
657
|
}
|
|
626
658
|
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
// Cross-source enumeration (workspace + plugin-bundled apps)
|
|
661
|
+
// ---------------------------------------------------------------------------
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Where an app is provided from. Workspace apps are user-created and live under
|
|
665
|
+
* `<workspace>/data/apps/`; plugin apps are bundled by an installed plugin
|
|
666
|
+
* under `<workspace>/plugins/<name>/apps/`.
|
|
667
|
+
*/
|
|
668
|
+
export type AppOrigin =
|
|
669
|
+
| { readonly kind: "workspace" }
|
|
670
|
+
| { readonly kind: "plugin"; readonly pluginName: string };
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Id prefix marking a plugin-bundled app. A plugin app's id encodes its
|
|
674
|
+
* location: `plugins~<pluginName>~<appDir>`, which maps to
|
|
675
|
+
* `<workspace>/plugins/<pluginName>/apps/<appDir>` (the `apps/` segment is
|
|
676
|
+
* implied). Unlike workspace apps — whose id is an opaque UUID looked up by
|
|
677
|
+
* scanning — a plugin app is resolved by a direct path build.
|
|
678
|
+
*
|
|
679
|
+
* The delimiter is `~` (a URL-unreserved character), not `/`, so the id is a
|
|
680
|
+
* single URL path segment: it survives `:id` route params and proxies without
|
|
681
|
+
* percent-encoding (`%2F` in a path is widely mishandled).
|
|
682
|
+
*/
|
|
683
|
+
const PLUGIN_APP_ID_PREFIX = "plugins~";
|
|
684
|
+
const PLUGIN_APP_ID_SEP = "~";
|
|
685
|
+
|
|
686
|
+
/** Build the addressable id for a plugin-bundled app. */
|
|
687
|
+
function pluginAppId(pluginName: string, appDir: string): string {
|
|
688
|
+
return `${PLUGIN_APP_ID_PREFIX}${pluginName}${PLUGIN_APP_ID_SEP}${appDir}`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** True when an app id addresses a plugin-bundled app (vs a workspace app). */
|
|
692
|
+
export function isPluginAppId(id: string): boolean {
|
|
693
|
+
return id.startsWith(PLUGIN_APP_ID_PREFIX);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** An app paired with the absolute path to its source and its origin. */
|
|
697
|
+
export interface EnumeratedApp {
|
|
698
|
+
/**
|
|
699
|
+
* Addressable app id: an opaque UUID for workspace apps, or
|
|
700
|
+
* `plugins~<name>~<app>` for plugin-bundled apps.
|
|
701
|
+
*/
|
|
702
|
+
readonly id: string;
|
|
703
|
+
/** Human-readable app name. */
|
|
704
|
+
readonly name: string;
|
|
705
|
+
/** Absolute path to the app's source directory. */
|
|
706
|
+
readonly sourcePath: string;
|
|
707
|
+
/** Where the app is provided from. */
|
|
708
|
+
readonly origin: AppOrigin;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Enumerate apps bundled under a single plugin's `apps/` directory. Each
|
|
713
|
+
* immediate subdirectory is one app: its directory name is the app name and
|
|
714
|
+
* its path is the source.
|
|
715
|
+
*/
|
|
716
|
+
function listAppsForPlugin(
|
|
717
|
+
pluginName: string,
|
|
718
|
+
pluginDir: string,
|
|
719
|
+
): EnumeratedApp[] {
|
|
720
|
+
const appsDir = join(pluginDir, "apps");
|
|
721
|
+
let entries: string[];
|
|
722
|
+
try {
|
|
723
|
+
entries = readdirSync(appsDir);
|
|
724
|
+
} catch {
|
|
725
|
+
// No apps/ directory (or unreadable) — this plugin bundles no apps.
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
728
|
+
const apps: EnumeratedApp[] = [];
|
|
729
|
+
for (const entry of entries) {
|
|
730
|
+
const appDir = join(appsDir, entry);
|
|
731
|
+
try {
|
|
732
|
+
// statSync follows symlinks so a symlinked app directory still counts.
|
|
733
|
+
if (!statSync(appDir).isDirectory()) {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
} catch {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
apps.push({
|
|
740
|
+
id: pluginAppId(pluginName, entry),
|
|
741
|
+
name: entry,
|
|
742
|
+
sourcePath: appDir,
|
|
743
|
+
origin: { kind: "plugin", pluginName },
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
return apps;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Enumerate apps bundled by installed, enabled plugins under
|
|
751
|
+
* `<workspace>/plugins/<name>/apps/`.
|
|
752
|
+
*
|
|
753
|
+
* Plugin discovery mirrors the plugin loader's `scanPlugins`: a plugin is an
|
|
754
|
+
* entry that resolves to a directory (following symlinks) and carries a
|
|
755
|
+
* `package.json` manifest. Stray directories without a manifest are ignored,
|
|
756
|
+
* and disabled plugins (those with a `.disabled` sentinel) contribute nothing,
|
|
757
|
+
* matching how their other surfaces (tools, hooks, routes) are gated.
|
|
758
|
+
*/
|
|
759
|
+
export function listPluginApps(): EnumeratedApp[] {
|
|
760
|
+
const pluginsRoot = getWorkspacePluginsDir();
|
|
761
|
+
if (!existsSync(pluginsRoot)) {
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
let entries: string[];
|
|
765
|
+
try {
|
|
766
|
+
entries = readdirSync(pluginsRoot);
|
|
767
|
+
} catch {
|
|
768
|
+
return [];
|
|
769
|
+
}
|
|
770
|
+
const apps: EnumeratedApp[] = [];
|
|
771
|
+
for (const name of entries) {
|
|
772
|
+
const pluginDir = join(pluginsRoot, name);
|
|
773
|
+
try {
|
|
774
|
+
if (!statSync(pluginDir).isDirectory()) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
} catch {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (!existsSync(join(pluginDir, "package.json"))) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (isPluginDisabled(name)) {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
apps.push(...listAppsForPlugin(name, pluginDir));
|
|
787
|
+
}
|
|
788
|
+
return apps;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Enumerate every app the assistant can surface, tagged with its origin:
|
|
793
|
+
* workspace apps (user-created) followed by apps bundled by installed plugins.
|
|
794
|
+
*/
|
|
795
|
+
export function listAllApps(): EnumeratedApp[] {
|
|
796
|
+
const workspaceApps: EnumeratedApp[] = listApps().map((definition) => ({
|
|
797
|
+
id: definition.id,
|
|
798
|
+
name: definition.name,
|
|
799
|
+
sourcePath: getAppDirPath(definition.id),
|
|
800
|
+
origin: { kind: "workspace" as const },
|
|
801
|
+
}));
|
|
802
|
+
return [...workspaceApps, ...listPluginApps()];
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** An app id resolved to its on-disk source and metadata. */
|
|
806
|
+
export interface ResolvedAppSource {
|
|
807
|
+
/** The id that was resolved (echoed back for the caller). */
|
|
808
|
+
readonly id: string;
|
|
809
|
+
/** Human-readable app name. */
|
|
810
|
+
readonly name: string;
|
|
811
|
+
/** Filesystem directory stem — the workspace slug or the plugin app dir. */
|
|
812
|
+
readonly dirName: string;
|
|
813
|
+
/** Absolute path to the app's source directory. */
|
|
814
|
+
readonly sourceDir: string;
|
|
815
|
+
/** Where the app is provided from. */
|
|
816
|
+
readonly origin: AppOrigin;
|
|
817
|
+
/** 1 = single-file (index.html at root); 2 = multi-file (compiled dist/). */
|
|
818
|
+
readonly formatVersion: number;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Validate a single plugin-app id path segment (plugin name or app directory).
|
|
823
|
+
* Rejects empty, traversal, separators, and untrimmed values so an id can be
|
|
824
|
+
* mapped to a filesystem path without escaping the plugins root.
|
|
825
|
+
*/
|
|
826
|
+
function isSafeIdSegment(segment: string): boolean {
|
|
827
|
+
return (
|
|
828
|
+
segment.length > 0 &&
|
|
829
|
+
segment === segment.trim() &&
|
|
830
|
+
!segment.includes("/") &&
|
|
831
|
+
!segment.includes("\\") &&
|
|
832
|
+
!segment.includes("..") &&
|
|
833
|
+
segment !== "."
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Resolve an app id to its on-disk source, for both workspace apps (opaque
|
|
839
|
+
* UUID, looked up via {@link getApp}) and plugin-bundled apps
|
|
840
|
+
* (`plugins~<name>~<app>`, resolved by direct path build). Returns null when
|
|
841
|
+
* the app does not exist, or when a plugin id fails the same installed-plugin
|
|
842
|
+
* gates as discovery (directory, `package.json` manifest, not disabled).
|
|
843
|
+
*/
|
|
844
|
+
export function resolveAppSource(id: string): ResolvedAppSource | null {
|
|
845
|
+
if (id.startsWith(PLUGIN_APP_ID_PREFIX)) {
|
|
846
|
+
const rest = id.slice(PLUGIN_APP_ID_PREFIX.length);
|
|
847
|
+
// Split on the first delimiter: the plugin name is kebab-case (no `~`), so
|
|
848
|
+
// everything after it is the app directory (which may itself contain `~`).
|
|
849
|
+
const sep = rest.indexOf(PLUGIN_APP_ID_SEP);
|
|
850
|
+
if (sep === -1) {
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
const pluginName = rest.slice(0, sep);
|
|
854
|
+
const appDirName = rest.slice(sep + PLUGIN_APP_ID_SEP.length);
|
|
855
|
+
// Both must be safe single path components — the app segment, used as a
|
|
856
|
+
// directory name, must not smuggle in separators or traversal.
|
|
857
|
+
if (!isSafeIdSegment(pluginName) || !isSafeIdSegment(appDirName)) {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
const pluginDir = join(getWorkspacePluginsDir(), pluginName);
|
|
861
|
+
try {
|
|
862
|
+
if (!statSync(pluginDir).isDirectory()) {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
} catch {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
if (!existsSync(join(pluginDir, "package.json"))) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
if (isPluginDisabled(pluginName)) {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
const sourceDir = join(pluginDir, "apps", appDirName);
|
|
875
|
+
try {
|
|
876
|
+
if (!statSync(sourceDir).isDirectory()) {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
} catch {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
// Single-file apps ship index.html at the root; anything else is treated
|
|
883
|
+
// as multi-file (served from a compiled dist/).
|
|
884
|
+
const formatVersion = existsSync(join(sourceDir, "index.html")) ? 1 : 2;
|
|
885
|
+
return {
|
|
886
|
+
id,
|
|
887
|
+
name: appDirName,
|
|
888
|
+
dirName: appDirName,
|
|
889
|
+
sourceDir,
|
|
890
|
+
origin: { kind: "plugin", pluginName },
|
|
891
|
+
formatVersion,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const app = getApp(id);
|
|
896
|
+
if (!app) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
const { dirName } = resolveAppDir(id);
|
|
900
|
+
return {
|
|
901
|
+
id,
|
|
902
|
+
name: app.name,
|
|
903
|
+
dirName,
|
|
904
|
+
sourceDir: getAppDirPath(id),
|
|
905
|
+
origin: { kind: "workspace" },
|
|
906
|
+
formatVersion: app.formatVersion ?? 1,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
627
910
|
export function updateApp(
|
|
628
911
|
id: string,
|
|
629
912
|
updates: Partial<
|
|
@@ -880,6 +1163,23 @@ export function readAppFileBytes(appId: string, path: string): Buffer {
|
|
|
880
1163
|
return readFileSync(resolved);
|
|
881
1164
|
}
|
|
882
1165
|
|
|
1166
|
+
/**
|
|
1167
|
+
* Read a file as raw bytes from an app directory addressed by its absolute
|
|
1168
|
+
* path (rather than by a workspace app id). Path is validated to prevent
|
|
1169
|
+
* traversal. Used to serve assets for plugin-bundled apps, whose source dir is
|
|
1170
|
+
* resolved via {@link resolveAppSource}.
|
|
1171
|
+
*/
|
|
1172
|
+
export function readAppFileBytesFromDir(
|
|
1173
|
+
sourceDir: string,
|
|
1174
|
+
path: string,
|
|
1175
|
+
): Buffer {
|
|
1176
|
+
const resolved = validateFilePathInDir(sourceDir, path);
|
|
1177
|
+
if (!existsSync(resolved)) {
|
|
1178
|
+
throw new Error(`File not found: ${path}`);
|
|
1179
|
+
}
|
|
1180
|
+
return readFileSync(resolved);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
883
1183
|
/**
|
|
884
1184
|
* Write a file to the app directory.
|
|
885
1185
|
* Auto-creates intermediate directories. Path is validated to prevent traversal.
|
package/src/cli/AGENTS.md
CHANGED
|
@@ -154,6 +154,23 @@ registerFooCommand(program);
|
|
|
154
154
|
`console.log` results directly; that bypasses the convention and
|
|
155
155
|
breaks downstream scripting.
|
|
156
156
|
|
|
157
|
+
**`--json` is global.** `registerGlobalJsonOption` (called once in
|
|
158
|
+
`program.ts` after every command is registered) attaches `--json` to
|
|
159
|
+
every **leaf** command in the tree, so a caller can append `--json` to
|
|
160
|
+
any command it actually invokes without hitting "unknown option". Only
|
|
161
|
+
leaves get it: Commander consumes a recognized option at the outermost
|
|
162
|
+
command that declares it, even when the flag appears after the
|
|
163
|
+
subcommand name, so a group-level `--json` would swallow
|
|
164
|
+
`clients list --json` before the `list` action reads `opts.json`. You
|
|
165
|
+
therefore do **not** need to declare `--json` yourself — but a command
|
|
166
|
+
only _honors_ it when its action routes output through
|
|
167
|
+
`writeOutput`/`shouldOutputJson` (or reads `opts.json`). When a command
|
|
168
|
+
emits bespoke human-readable text, branch on `shouldOutputJson(cmd)` and
|
|
169
|
+
emit a JSON object in the `--json` case (see `commands/status.ts`).
|
|
170
|
+
Declare `--json` explicitly in the command's `.help` module only when
|
|
171
|
+
you want a more specific description than the generic one; the global
|
|
172
|
+
registration skips any leaf that already declares it.
|
|
173
|
+
|
|
157
174
|
### Anti-patterns
|
|
158
175
|
|
|
159
176
|
- ❌ Hoisting a daemon import — `import { x } from "../../runtime/foo.js"`
|
|
@@ -33,6 +33,7 @@ let mockResponses: Array<{
|
|
|
33
33
|
ok: boolean;
|
|
34
34
|
result?: unknown;
|
|
35
35
|
error?: string;
|
|
36
|
+
statusCode?: number;
|
|
36
37
|
}> = [];
|
|
37
38
|
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
@@ -44,6 +45,10 @@ mock.module("../../../ipc/cli-client.js", () => ({
|
|
|
44
45
|
ipcCalls.push({ method, params });
|
|
45
46
|
return mockResponses.shift() ?? { ok: true, result: null };
|
|
46
47
|
},
|
|
48
|
+
exitFromIpcResult: (r: { error?: string }) => {
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
if (r.error) process.stderr.write(r.error);
|
|
51
|
+
},
|
|
47
52
|
}));
|
|
48
53
|
|
|
49
54
|
mock.module("../../../util/logger.js", () => ({
|
|
@@ -179,6 +184,56 @@ describe("gateway logs tail — IPC params", () => {
|
|
|
179
184
|
});
|
|
180
185
|
});
|
|
181
186
|
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// gateway status
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
describe("gateway status", () => {
|
|
192
|
+
test("--json passes through the connected tunnel payload", async () => {
|
|
193
|
+
mockResponses.push({
|
|
194
|
+
ok: true,
|
|
195
|
+
result: { tunnel: "https://abc123.vellum.ai" },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const { stdout, exitCode } = await runCommand([
|
|
199
|
+
"gateway",
|
|
200
|
+
"status",
|
|
201
|
+
"--json",
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
expect(exitCode).toBe(0);
|
|
205
|
+
expect(ipcCalls[0].method).toBe("gateway_status");
|
|
206
|
+
expect(JSON.parse(stdout.trim())).toEqual({
|
|
207
|
+
tunnel: "https://abc123.vellum.ai",
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("--json emits {} when no tunnel is connected", async () => {
|
|
212
|
+
mockResponses.push({ ok: true, result: {} });
|
|
213
|
+
|
|
214
|
+
const { stdout, exitCode } = await runCommand([
|
|
215
|
+
"gateway",
|
|
216
|
+
"status",
|
|
217
|
+
"--json",
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
expect(exitCode).toBe(0);
|
|
221
|
+
expect(JSON.parse(stdout.trim())).toEqual({});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("exits non-zero when the gateway is not running", async () => {
|
|
225
|
+
mockResponses.push({
|
|
226
|
+
ok: false,
|
|
227
|
+
error: "Gateway is not running or is unreachable over IPC.",
|
|
228
|
+
statusCode: 503,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const { exitCode } = await runCommand(["gateway", "status"]);
|
|
232
|
+
|
|
233
|
+
expect(exitCode).not.toBe(0);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
182
237
|
// ---------------------------------------------------------------------------
|
|
183
238
|
// Pretty-print output
|
|
184
239
|
// ---------------------------------------------------------------------------
|
|
@@ -4,15 +4,21 @@ import type { CliCommandHelp } from "../lib/cli-command-help.js";
|
|
|
4
4
|
|
|
5
5
|
export const appsHelp: CliCommandHelp = {
|
|
6
6
|
name: "apps",
|
|
7
|
-
description: "Inspect
|
|
7
|
+
description: "Inspect apps and their on-disk source",
|
|
8
8
|
helpText: `
|
|
9
|
-
Apps are
|
|
10
|
-
directory (~/.vellum/apps/<slug>/). Each app has a human-readable name and a
|
|
9
|
+
Apps are mini-applications the assistant can surface. Each has a name and a
|
|
11
10
|
source directory holding its HTML/TSX definition, pages, and records.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
Apps come from two places, distinguished by their source path:
|
|
13
|
+
workspace User-created apps under the workspace apps directory
|
|
14
|
+
(~/.vellum/apps/<slug>/).
|
|
15
|
+
plugin Apps bundled by an installed plugin, each a directory under
|
|
16
|
+
~/.vellum/plugins/<name>/apps/<app>/. Stray directories without
|
|
17
|
+
a plugin package.json, and disabled plugins, contribute nothing.
|
|
18
|
+
|
|
19
|
+
This surface is read-only. Workspace apps are created, edited, and deleted
|
|
20
|
+
through the app-builder skill and its tools (app-create, app-update,
|
|
21
|
+
app-delete); plugin apps are owned by their plugin.
|
|
16
22
|
|
|
17
23
|
Examples:
|
|
18
24
|
$ assistant apps list
|
|
@@ -25,12 +31,12 @@ Examples:
|
|
|
25
31
|
{ flags: "--json", description: "Machine-readable JSON output" },
|
|
26
32
|
],
|
|
27
33
|
helpText: `
|
|
28
|
-
Lists every app with its name and the absolute path to its source directory
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
Lists every app with its name and the absolute path to its source directory —
|
|
35
|
+
the path itself shows whether the app is a workspace app or bundled by a plugin
|
|
36
|
+
(~/.vellum/plugins/<name>/apps/...). Workspace apps are listed first, then
|
|
37
|
+
plugin-bundled apps; both are sorted by name.
|
|
31
38
|
|
|
32
|
-
Pass --json for
|
|
33
|
-
last-updated time.
|
|
39
|
+
Pass --json for machine-readable output.
|
|
34
40
|
|
|
35
41
|
Examples:
|
|
36
42
|
$ assistant apps list
|
package/src/cli/commands/apps.ts
CHANGED
|
@@ -8,9 +8,6 @@ import { appsHelp } from "./apps.help.js";
|
|
|
8
8
|
interface AppListEntry {
|
|
9
9
|
name: string;
|
|
10
10
|
source: string;
|
|
11
|
-
id: string;
|
|
12
|
-
formatVersion: number;
|
|
13
|
-
updatedAt: number;
|
|
14
11
|
}
|
|
15
12
|
|
|
16
13
|
export function registerAppsCommand(program: Command): void {
|
|
@@ -24,17 +21,10 @@ export function registerAppsCommand(program: Command): void {
|
|
|
24
21
|
subcommand(apps, "list").action(async (opts: { json?: boolean }) => {
|
|
25
22
|
// Lazy-import the app store so the daemon module graph loads only when
|
|
26
23
|
// this command runs (cli/no-daemon-internals).
|
|
27
|
-
const {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.map((app) => ({
|
|
32
|
-
name: app.name,
|
|
33
|
-
source: getAppDirPath(app.id),
|
|
34
|
-
id: app.id,
|
|
35
|
-
formatVersion: app.formatVersion ?? 1,
|
|
36
|
-
updatedAt: app.updatedAt,
|
|
37
|
-
}))
|
|
24
|
+
const { listAllApps } = await import("../../apps/app-store.js");
|
|
25
|
+
|
|
26
|
+
const entries: AppListEntry[] = listAllApps()
|
|
27
|
+
.map(({ name, sourcePath }) => ({ name, source: sourcePath }))
|
|
38
28
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
39
29
|
|
|
40
30
|
if (opts.json) {
|
|
@@ -48,7 +38,7 @@ export function registerAppsCommand(program: Command): void {
|
|
|
48
38
|
}
|
|
49
39
|
|
|
50
40
|
// Name and source lead; align the name into a column so the source
|
|
51
|
-
// path
|
|
41
|
+
// path (which also identifies the app's origin) stays scannable.
|
|
52
42
|
const nameWidth = Math.max(4, ...entries.map((e) => e.name.length));
|
|
53
43
|
log.info(`Apps (${entries.length}):\n`);
|
|
54
44
|
log.info(` ${"NAME".padEnd(nameWidth)} SOURCE`);
|
|
@@ -11,11 +11,41 @@ manages trust rules, routes traffic to the assistant, and records
|
|
|
11
11
|
structured logs for all inbound activity.
|
|
12
12
|
|
|
13
13
|
Examples:
|
|
14
|
+
$ assistant gateway status
|
|
14
15
|
$ assistant gateway logs tail
|
|
15
16
|
$ assistant gateway logs tail -n 50
|
|
16
17
|
$ assistant gateway logs tail --level warn
|
|
17
18
|
$ assistant gateway logs tail --module cors`,
|
|
18
19
|
subcommands: [
|
|
20
|
+
{
|
|
21
|
+
name: "status",
|
|
22
|
+
description: "Show gateway status (public tunnel state)",
|
|
23
|
+
options: [
|
|
24
|
+
{
|
|
25
|
+
flags: "--json",
|
|
26
|
+
description: "Machine-readable compact JSON output",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
helpText: `
|
|
30
|
+
Reports the live status of the gateway's public tunnel — the outbound
|
|
31
|
+
public-ingress transport (currently Velay).
|
|
32
|
+
|
|
33
|
+
The tunnel is ONLY used to route inbound Twilio webhooks and live voice/audio
|
|
34
|
+
WebSockets through to this assistant. It has nothing to do with platform
|
|
35
|
+
credentials, the managed LLM proxy, or text channels. If you are not using
|
|
36
|
+
Twilio voice or live audio, a "not connected" tunnel is expected and harmless.
|
|
37
|
+
|
|
38
|
+
JSON output (--json):
|
|
39
|
+
{ "tunnel": "<public-url>" } when a tunnel is connected
|
|
40
|
+
{ } when the gateway is running but no tunnel is up
|
|
41
|
+
|
|
42
|
+
If the gateway itself is not running, the command exits non-zero with an error
|
|
43
|
+
rather than reporting an empty tunnel.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
$ assistant gateway status
|
|
47
|
+
$ assistant gateway status --json`,
|
|
48
|
+
},
|
|
19
49
|
{
|
|
20
50
|
name: "logs",
|
|
21
51
|
description: "Gateway log operations",
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
* `assistant gateway` CLI namespace.
|
|
3
3
|
*
|
|
4
4
|
* Subcommands:
|
|
5
|
+
* status — Show the gateway's public tunnel status via the daemon IPC proxy.
|
|
5
6
|
* logs tail — Show the last N gateway log entries via the daemon IPC proxy.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { Command } from "commander";
|
|
9
10
|
|
|
10
|
-
import { cliIpcCall } from "../../ipc/cli-client.js";
|
|
11
|
+
import { cliIpcCall, exitFromIpcResult } from "../../ipc/cli-client.js";
|
|
11
12
|
import { applyCommandHelp, subcommand } from "../lib/cli-command-help.js";
|
|
12
13
|
import { registerCommand } from "../lib/register-command.js";
|
|
13
14
|
import { log } from "../logger.js";
|
|
15
|
+
import { shouldOutputJson, writeOutput } from "../output.js";
|
|
14
16
|
import { gatewayHelp } from "./gateway.help.js";
|
|
15
17
|
|
|
16
18
|
// -- Types --------------------------------------------------------------------
|
|
@@ -23,6 +25,10 @@ interface PinoEntry {
|
|
|
23
25
|
[key: string]: unknown;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
interface GatewayStatusResult {
|
|
29
|
+
tunnel?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
// -- Helpers ------------------------------------------------------------------
|
|
27
33
|
|
|
28
34
|
function formatTime(ms: number): string {
|
|
@@ -66,6 +72,39 @@ export function registerGatewayCommand(program: Command): void {
|
|
|
66
72
|
build: (gateway) => {
|
|
67
73
|
applyCommandHelp(gateway, gatewayHelp);
|
|
68
74
|
|
|
75
|
+
// -----------------------------------------------------------------------
|
|
76
|
+
// status
|
|
77
|
+
// -----------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
subcommand(gateway, "status").action(async (_opts, cmd: Command) => {
|
|
80
|
+
const r = await cliIpcCall<GatewayStatusResult>("gateway_status", {});
|
|
81
|
+
if (!r.ok)
|
|
82
|
+
return exitFromIpcResult(
|
|
83
|
+
{ ok: false, error: r.error, statusCode: r.statusCode },
|
|
84
|
+
cmd,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const result = r.result!;
|
|
88
|
+
|
|
89
|
+
if (shouldOutputJson(cmd)) {
|
|
90
|
+
writeOutput(cmd, result);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (result.tunnel) {
|
|
95
|
+
log.info(`Tunnel: connected (${result.tunnel})`);
|
|
96
|
+
} else {
|
|
97
|
+
log.info("Tunnel: not connected");
|
|
98
|
+
}
|
|
99
|
+
log.info(
|
|
100
|
+
" The public tunnel is only used to route inbound Twilio webhooks and",
|
|
101
|
+
);
|
|
102
|
+
log.info(
|
|
103
|
+
" live voice/audio WebSockets. It is not needed for text channels or",
|
|
104
|
+
);
|
|
105
|
+
log.info(" the managed LLM proxy.");
|
|
106
|
+
});
|
|
107
|
+
|
|
69
108
|
const logs = subcommand(gateway, "logs");
|
|
70
109
|
|
|
71
110
|
subcommand(logs, "tail").action(async (opts) => {
|